traveller_rpg 0.0.0.4

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: 5641a18846aa83312b6821006edc36d5add290b17263c4258ad76413b8891993
4
+ data.tar.gz: 2041763e41ee95d19f4917ff3d996f53820e544caa63655d6e8a5d7cbf361ad9
5
+ SHA512:
6
+ metadata.gz: 2ebedfc2b15be64c0a617b376640397a42fccce3d1df8948da6f2c340aa5dbc60ca4cbe28ff3c20bd1e52df9b10867c97d83412e300f49bc05b1b3299c111cda
7
+ data.tar.gz: f21f01d409cd0f3169126e744527f35ee88d9f545974cd57d548082270b94f0e4b7350d0e152ab46eeb999741dcee110adf4a34aaae3c4ddc27ba48a1e2a795e
data/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # Traveller RPG
2
+
3
+ This work was inspired by
4
+ https://github.com/LeamHall/CT_Character_Generator which is also the origin
5
+ for some of the `data/*.txt` files. Most of the behavior is based on
6
+ information available from http://www.traveller-srd.com/core-rules
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new :test do |t|
4
+ t.pattern = "test/*.rb"
5
+ t.warning = true
6
+ end
7
+
8
+ begin
9
+ require 'buildar'
10
+
11
+ Buildar.new do |b|
12
+ b.gemspec_file = 'traveller_rpg.gemspec'
13
+ b.version_file = 'VERSION'
14
+ b.use_git = true
15
+ end
16
+ rescue LoadError
17
+ warn "buildar tasks unavailable"
18
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0.4
data/bin/chargen ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pry'
4
+ require 'traveller_rpg'
5
+ require 'traveller_rpg/careers'
6
+ require 'traveller_rpg/generator'
7
+ require 'traveller_rpg/career_path'
8
+
9
+ char = TravellerRPG::Generator.character
10
+ path = TravellerRPG::CareerPath.new(char)
11
+
12
+ careers = %w{Scout}
13
+
14
+ loop {
15
+ careers = careers.select { |c| path.eligible? c }
16
+ if careers.empty?
17
+ puts "No eligible careers available!"
18
+ break
19
+ end
20
+ career = TravellerRPG.choose("Choose a career", *careers)
21
+ career = TravellerRPG.career_class(career).new(char)
22
+
23
+ # skip draft / drifter for now
24
+ accepted = false
25
+ accepted = path.apply(career) while !accepted
26
+
27
+ #
28
+ loop {
29
+ path.run_term
30
+ break unless path.active_career
31
+ break if career.must_exit?
32
+ next if career.must_remain?
33
+ break if TravellerRPG.choose("Muster out?", :yes, :no) == :yes
34
+ }
35
+ path.muster_out if path.active_career
36
+
37
+ puts char.desc.pretty_inspect
38
+ puts char.stats.pretty_inspect
39
+ puts char.skills.pretty_inspect
40
+ puts char.stuff.pretty_inspect
41
+
42
+ break if TravellerRPG.choose("Exit career mode?", :yes, :no) == :yes
43
+ }
44
+
45
+ binding.pry if ENV['PRY']
46
+
47
+ puts "DONE"
@@ -0,0 +1,269 @@
1
+ require 'traveller_rpg'
2
+
3
+ module TravellerRPG
4
+ class Career
5
+ class Error < RuntimeError; end
6
+ class UnknownAssignment < Error; end
7
+ class MusterError < Error; end
8
+
9
+ def self.advanced_skills?(stats)
10
+ stats.education >= 8
11
+ end
12
+
13
+ TERM_YEARS = 4
14
+
15
+ QUALIFY_CHECK = 5
16
+ SURVIVAL_CHECK = 6
17
+ ADVANCEMENT_CHECK = 9
18
+
19
+ STATS = Array.new(6) { :default }
20
+ SERVICE_SKILLS = Array.new(6) { :default }
21
+ ADVANCED_SKILLS = Array.new(6) { :default }
22
+ SPECIALIST_SKILLS = { default: Array.new(6) { :default } }
23
+ RANKS = {}
24
+
25
+ EVENTS = {
26
+ 2 => nil,
27
+ 3 => nil,
28
+ 4 => nil,
29
+ 5 => nil,
30
+ 6 => nil,
31
+ 7 => nil,
32
+ 8 => nil,
33
+ 9 => nil,
34
+ 10 => nil,
35
+ 11 => nil,
36
+ 12 => nil,
37
+ }
38
+
39
+ MISHAPS = {
40
+ 1 => nil,
41
+ 2 => nil,
42
+ 3 => nil,
43
+ 4 => nil,
44
+ 5 => nil,
45
+ 6 => nil,
46
+ }
47
+
48
+ CASH = {
49
+ 2 => -500,
50
+ 3 => -100,
51
+ 4 => 200,
52
+ 5 => 400,
53
+ 6 => 600,
54
+ 7 => 800,
55
+ 8 => 1000,
56
+ 9 => 2000,
57
+ 10 => 4000,
58
+ 11 => 8000,
59
+ 12 => 16000,
60
+ }
61
+
62
+ attr_reader :stats, :skills, :benefits
63
+ attr_accessor :term, :active, :rank
64
+
65
+ def initialize(char, assignment: nil, term: 0, active: false, rank: 0,
66
+ benefits: {})
67
+ @char = char
68
+ @assignment = assignment
69
+ if @assignment and !self.class::SPECIALIST_SKILLS.key?(@assignment)
70
+ raise(UnknownAssignment, assignment.inspect)
71
+ end
72
+
73
+ # career tracking
74
+ @term = term
75
+ @active = active
76
+ @rank = rank
77
+ @benefits = benefits # acquired equipment, ships / shares
78
+ @term_mandate = nil
79
+ end
80
+
81
+ def assignment
82
+ @assignment ||= TravellerRPG.choose("Choose a specialty:",
83
+ *self.class::SPECIALIST_SKILLS.keys)
84
+ end
85
+
86
+ def active?
87
+ !!@active
88
+ end
89
+
90
+ def qualify_check?(career_count, dm: 0)
91
+ dm += -1 * career_count
92
+ roll = TravellerRPG.roll('2d6')
93
+ puts format("Qualify check: rolled %i (DM %i) against %i",
94
+ roll, dm, self.class::QUALIFY_CHECK)
95
+ (roll + dm) >= self.class::QUALIFY_CHECK
96
+ end
97
+
98
+ def survival_check?(dm: 0)
99
+ roll = TravellerRPG.roll('2d6')
100
+ puts format("Survival check: rolled %i (DM %i) against %i",
101
+ roll, dm, self.class::SURVIVAL_CHECK)
102
+ (roll + dm) >= self.class::SURVIVAL_CHECK
103
+ end
104
+
105
+ def advancement_check?(roll: nil, dm: 0)
106
+ roll ||= TravellerRPG.roll('2d6')
107
+ puts format("Advancement check: rolled %i (DM %i) against %i",
108
+ roll, dm, self.class::ADVANCEMENT_CHECK)
109
+ (roll + dm) >= self.class::ADVANCEMENT_CHECK
110
+ end
111
+
112
+ # any skills obtained start at level 1
113
+ def training_roll
114
+ roll = TravellerRPG.roll('d6')
115
+ @char.log "Training roll (d6): #{roll}"
116
+ choices = [:stats, :service, :specialist]
117
+ choices << :advanced if self.class.advanced_skills?(@char.stats)
118
+ choices << :officer if self.respond_to?(:officer) and self.officer
119
+ choice = TravellerRPG.choose("Choose training regimen:", *choices)
120
+ case choice
121
+ when :stats
122
+ stat = self.class::STATS.fetch(roll - 1)
123
+ if @char.stats.respond_to?(stat)
124
+ @char.stats.boost(stat => 1)
125
+ @char.log "Trained #{stat} to #{@char.stats.send(stat)}"
126
+ else
127
+ raise "bad stat: #{stat}" unless TravellerRPG::SKILLS.key?(stat)
128
+ # stat is likely :jack_of_all_trades skill
129
+ @char.skills[stat] ||= 0
130
+ @char.skills[stat] += 1
131
+ @char.log "Trained (stats) skill #{stat} to #{@char.skills[stat]}"
132
+ end
133
+ when :service
134
+ svc = self.class::SERVICE_SKILLS.fetch(roll - 1)
135
+ @char.skills[svc] ||= 0
136
+ @char.skills[svc] += 1
137
+ @char.log "Trained service skill #{svc} to #{@char.skills[svc]}"
138
+ when :specialist
139
+ spec =
140
+ self.class::SPECIALIST_SKILLS.fetch(self.assignment).fetch(roll - 1)
141
+ @char.skills[spec] ||= 0
142
+ @char.skills[spec] += 1
143
+ @char.log "Trained #{@assignment} specialist skill #{spec} " +
144
+ "to #{@char.skills[spec]}"
145
+ when :advanced
146
+ adv = self.class::ADVANCED_SKILLS.fetch(roll - 1)
147
+ @char.skills[adv] ||= 0
148
+ @char.skills[adv] += 1
149
+ @char.log "Trained advanced skill #{adv} to #{@char.skills[adv]}"
150
+ when :officer
151
+ off = self.class::OFFICER_SKILLS.fetch(roll - 1)
152
+ @char.skills[off] ||= 0
153
+ @char.skills[off] += 1
154
+ @char.log "Trained officer skill #{off} to #{@char.skills[off]}"
155
+ end
156
+ end
157
+
158
+ def event_roll(dm: 0)
159
+ roll = TravellerRPG.roll('2d6')
160
+ clamped = (roll + dm).clamp(2, 12)
161
+ @char.log "Event roll (2d6): #{roll} + DM #{dm} = #{clamped}"
162
+ @char.log self.class::EVENTS.fetch(clamped)
163
+ # TODO: actually perform the event stuff
164
+ end
165
+
166
+ def mishap_roll
167
+ roll = TravellerRPG.roll('d6')
168
+ @char.log "Mishap roll (d6): #{roll}"
169
+ @char.log self.class::MISHAPS.fetch(roll)
170
+ # TODO: actually perform the mishap stuff
171
+ end
172
+
173
+ def cash_roll(dm: 0)
174
+ roll = TravellerRPG.roll('2d6')
175
+ clamped = (roll + dm).clamp(2, 12)
176
+ amount = self.class::CASH.fetch(clamped)
177
+ puts "Cash roll: #{roll} (DM #{dm}) = #{clamped} for #{amount}"
178
+ amount
179
+ end
180
+
181
+ def advance_rank
182
+ @rank += 1
183
+ @char.log "Advanced career to rank #{@rank}"
184
+ title, skill, level = self.rank_benefit
185
+ if title
186
+ @char.log "Awarded rank title: #{title}"
187
+ @char.log "Achieved rank skill: #{skill} #{level}"
188
+ @char.skills[skill] ||= 0
189
+ @char.skills[skill] = level if level > @char.skills[skill]
190
+ end
191
+ end
192
+
193
+ def must_remain?
194
+ @term_mandate == :must_remain
195
+ end
196
+
197
+ def must_exit?
198
+ @term_mandate == :must_exit
199
+ end
200
+
201
+ def run_term
202
+ raise(Error, "career is inactive") unless @active
203
+ raise(Error, "must exit") if self.must_exit?
204
+ @term += 1
205
+ @char.log format("%s term %i started, age %i",
206
+ self.name, @term, @char.age)
207
+ self.training_roll
208
+
209
+ if self.survival_check?
210
+ @char.log format("%s term %i was successful", self.name, @term)
211
+ @char.age TERM_YEARS
212
+
213
+ self.commission_roll if self.respond_to?(:commission_roll)
214
+
215
+ adv_roll = TravellerRPG.roll('2d6')
216
+ # TODO: DM?
217
+ if self.advancement_check?(roll: adv_roll)
218
+ self.advance_rank
219
+ self.training_roll
220
+ end
221
+ if adv_roll <= @term
222
+ @term_mandate = :must_exit
223
+ elsif adv_roll == 12
224
+ @term_mandate = :must_remain
225
+ else
226
+ @term_mandate = nil
227
+ end
228
+
229
+ self.event_roll
230
+ else
231
+ @char.log "#{self.name} career ended with a mishap!"
232
+ @char.age rand(TERM_YEARS) + 1
233
+ self.mishap_roll
234
+ @active = false
235
+ end
236
+ end
237
+
238
+ def retirement_bonus
239
+ @term >= 5 ? @term * 2000 : 0
240
+ end
241
+
242
+ def muster_out(dm: 0)
243
+ if @active
244
+ raise(MusterError, "career has not started") unless @term > 0
245
+ @active = false
246
+ cash_benefit = 0
247
+ @char.log "Muster out: #{self.name}"
248
+ dm += @char.skill_check?(:gambler, 1) ? 1 : 0
249
+ @term.clamp(0, 3).times {
250
+ cash_benefit += self.cash_roll(dm: dm)
251
+ }
252
+ @char.log "Cash benefit: #{cash_benefit}"
253
+ @char.log "Retirement bonus: #{self.retirement_bonus}"
254
+ @benefits[:cash] ||= 0
255
+ @benefits[:cash] += cash_benefit + self.retirement_bonus
256
+ @benefits
257
+ end
258
+ end
259
+
260
+ def name
261
+ self.class.name.split('::').last
262
+ end
263
+
264
+ # possibly nil
265
+ def rank_benefit
266
+ self.class::RANKS[@rank]
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,93 @@
1
+ require 'traveller_rpg/career'
2
+
3
+ module TravellerRPG
4
+ class CareerPath
5
+ class Error < RuntimeError; end
6
+ class Ineligible < Error; end
7
+
8
+ attr_reader :char, :careers, :active_career
9
+
10
+ def initialize(character)
11
+ @char = character
12
+ @char.log "Initiated new career path"
13
+ @careers = []
14
+ @active_career
15
+ end
16
+
17
+ def eligible?(career)
18
+ case career
19
+ when Career
20
+ return false if career.active?
21
+ cls = career.class
22
+ when String
23
+ cls = TravellerRPG.career_class(career)
24
+ end
25
+ !@careers.any? { |c| c.class == cls }
26
+ end
27
+
28
+ def apply(career)
29
+ raise(Ineligible, career.name) unless self.eligible?(career)
30
+ if career.qualify_check?(@careers.size)
31
+ @char.log "Qualified for #{career.name}"
32
+ self.enter(career)
33
+ else
34
+ @char.log "Did not qualify for #{career.name}"
35
+ false
36
+ end
37
+ end
38
+
39
+ def enter(career)
40
+ raise(Ineligible, career.name) unless self.eligible?(career)
41
+ raise(Error, "career is already active") if career.active?
42
+ raise(Error, "career has already started") unless career.term == 0
43
+ self.muster_out
44
+ @char.log "Entering new career: #{career.name}"
45
+ @active_career = career
46
+ @active_career.active = true
47
+ self.basic_training
48
+ self
49
+ end
50
+
51
+ def basic_training
52
+ return unless @active_career.term.zero?
53
+ if @careers.length.zero?
54
+ @active_career.class::SERVICE_SKILLS
55
+ else
56
+ [TravellerRPG.choose("Service skill",
57
+ *@active_career.class::SERVICE_SKILLS)]
58
+ end.each { |sym|
59
+ unless char.skills.key?(sym)
60
+ @char.log "Acquired basic training skill: #{sym} 0"
61
+ @char.skills[sym] = 0
62
+ end
63
+ }
64
+ end
65
+
66
+ def run_term
67
+ raise(Error, "no active career") unless @active_career
68
+ @active_career.run_term
69
+ unless @active_career.active?
70
+ @careers << @active_career
71
+ @active_career = nil
72
+ end
73
+ end
74
+
75
+ def muster_out
76
+ if @active_career
77
+ raise(Error, "career is inactive") unless @active_career.active?
78
+ raise(Error, "must remain") if @active_career.must_remain?
79
+ @char.add_stuff(@active_career.muster_out)
80
+ @careers << @active_career
81
+ @active_career = nil
82
+ end
83
+ end
84
+
85
+ def draft_term
86
+ @char.log "Drafted! (fake)"
87
+ end
88
+
89
+ def drifter_term
90
+ @char.log "Became a drifter (fake)"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,159 @@
1
+ require 'traveller_rpg'
2
+ require 'traveller_rpg/career'
3
+
4
+ module TravellerRPG
5
+ class MilitaryCareer < Career
6
+ COMMISSION_CHECK = 9
7
+ OFFICER_SKILLS = Array.new(6) { :default }
8
+ OFFICER_RANKS = {}
9
+
10
+ attr_reader :officer, :officer_rank
11
+
12
+ def initialize(char, **kwargs)
13
+ super(char, **kwargs)
14
+ @officer = false
15
+ end
16
+
17
+ def rank
18
+ @officer ? @officer_rank : @rank
19
+ end
20
+
21
+ def rank_benefit
22
+ @officer ? self.class::OFFICER_RANKS[@officer_rank] : super
23
+ end
24
+
25
+ def advance_rank
26
+ return super unless @officer
27
+ @officer_rank += 1
28
+ @char.log "Advanced career to officer rank #{@officer_rank}"
29
+ title, skill, level = self.rank_benefit
30
+ if title
31
+ @char.log "Awarded officer rank title: #{title}"
32
+ @char.log "Achieved officer rank skill: #{skill} #{level}"
33
+ @char.skills[skill] ||= 0
34
+ @char.skills[skill] = level if level > @char.skills[skill]
35
+ end
36
+ end
37
+
38
+ def commission_check?(dm = 0)
39
+ roll = TravellerRPG.roll('2d6')
40
+ puts format("Commission check: roll %i (DM %i) against %i",
41
+ roll, dm, self.class::COMMISSION_CHECK)
42
+ (roll + dm) >= self.class::COMMISSION_CHECK
43
+ end
44
+
45
+ def commission_roll(dm: 0)
46
+ return if @officer
47
+ if TravellerRPG.choose("Apply for commission?", :yes, :no) == :yes
48
+ if self.commission_check?
49
+ @char.log "Became an officer!"
50
+ @officer = true
51
+ @officer_rank = 1
52
+ else
53
+ @char.log "Commission was rejected"
54
+ end
55
+ end
56
+ end
57
+
58
+ def officer?
59
+ !!@officer
60
+ end
61
+ end
62
+
63
+ class Navy < MilitaryCareer
64
+ end
65
+
66
+ class Army < MilitaryCareer
67
+ end
68
+
69
+ class Marines < MilitaryCareer
70
+ end
71
+
72
+ class MerchantMarine < MilitaryCareer
73
+ end
74
+
75
+ class Agent < MilitaryCareer
76
+ end
77
+
78
+ class Scout < MilitaryCareer
79
+ # note, 6th "stat" is really a skill - jack of all trades
80
+ STATS = [:strength, :dexterity, :endurance,
81
+ :intelligence, :education, :jack_of_all_trades]
82
+ SERVICE_SKILLS = [:pilot_small_craft, :survival, :mechanic,
83
+ :astrogation, :comms, :gun_combat_group]
84
+ ADVANCED_SKILLS = [:medic, :navigation, :engineer,
85
+ :computers, :space_science, :jack_of_all_trades]
86
+
87
+ # made up by Rick
88
+ OFFICER_SKILLS = [:deception, :language_group, :investigate,
89
+ :remote_operations, :tactics_military, :leadership]
90
+ SPECIALIST_SKILLS = {
91
+ courier: [:comms, :sensors, :pilot_spacecraft,
92
+ :vacc_suit, :zero_g, :astrogation],
93
+ survey: [:sensors, :persuade, :pilot_small_craft,
94
+ :navigation, :diplomat, :streetwise],
95
+ exploration: [:sensors, :pilot_spacecraft, :pilot_small_craft,
96
+ :life_science_any, :stealth, :recon],
97
+ }
98
+
99
+ # key: roll; values: title, skill, skill_value
100
+ RANKS = {
101
+ 1 => [:scout, :vacc_suit, 1],
102
+ 3 => [:senior_scout, :pilot, 1],
103
+ }
104
+
105
+ EVENTS = {
106
+ 2 => 'Disaster! Roll on the mishap table but you are not ejected ' +
107
+ 'from career.',
108
+ 3 => 'Ambush! Choose Pilot 8+ to escape or Persuade 10+ to bargain. ' +
109
+ 'Gain an Enemy either way',
110
+ 4 => 'Survey an alien world. Choose Animals, Survival, Recon, or ' +
111
+ 'Life Sciences 1',
112
+ 5 => 'You perform an exemplary service. Gain a benefit roll with +1 DM',
113
+ 6 => 'You spend several years exploring the star system; ' +
114
+ 'Choose Atrogation, Navigation, Pilot (small craft) or Mechanic 1',
115
+ 7 => 'Life event. Roll on the Life Events table',
116
+ 8 => 'Gathered intelligence on an alien race. Roll Sensors 8+ or ' +
117
+ 'Deception 8+. Gain an ally in the Imperium and +2 DM to your ' +
118
+ 'next Advancement roll on success. Roll on the mishap table on ' +
119
+ 'failure, but you are not ejected from career.',
120
+ 9 => 'You rescue disaster survivors. Roll either Medic 8+ or ' +
121
+ 'Engineer 8+. Gain a Contact and +2 DM on next Advancement roll ' +
122
+ 'or else gain an Enemy',
123
+ 10 => 'You spend a great deal of time on the fringes of known space. ' +
124
+ 'Roll Survival 8+ or Pilot 8+. Gain a Contact in an alien race ' +
125
+ 'and one level in any skill, or else roll on the Mishap table.',
126
+ 11 => 'You serve as a courier for an important message for the ' +
127
+ 'Imperium. Gain one level of diplomat or take +4 DM to your ' +
128
+ 'next Advancement roll.',
129
+ 12 => 'You make an important discovery for the Imperium. Gain a ' +
130
+ 'career rank.',
131
+ }
132
+
133
+ MISHAPS = {
134
+ 1 => 'Severely injured in action. Roll twice on the Injury table ' +
135
+ 'or take a level 2 Injury.',
136
+ 2 => 'Suffer psychological damage. Reduce Intelligence or Social ' +
137
+ 'Standing by 1',
138
+ 3 => 'Your ship is damaged, and you have to hitch a ride back to ' +
139
+ 'your nearest scout base. Gain 1d6 Contacts and 1d3 Enemies.',
140
+ 4 => 'You inadvertently cause a conflict between the Imperium and ' +
141
+ 'a minor world or race. Gain a Rival and Diplomat 1.',
142
+ 5 => 'You have no idea what happened to you. Your ship was found ' +
143
+ 'drifting on the fringes of friendly space',
144
+ 6 => 'Injured. Roll on the Injury table.',
145
+ }
146
+
147
+ # not defined at http://www.traveller-srd.com/core-rules/careers/ :(
148
+ BENEFITS = {}
149
+
150
+ def qualify_check?(career_count)
151
+ @char.log format("Qualify DM is based on Intelligence %i",
152
+ @char.stats.intelligence)
153
+ super(career_count, dm: @char.class.stats_dm(@char.stats.intelligence))
154
+ end
155
+ end
156
+
157
+ class Drifter < Career
158
+ end
159
+ end
@@ -0,0 +1,128 @@
1
+ require 'traveller_rpg'
2
+
3
+ module TravellerRPG
4
+ class Character
5
+ Stats = Struct.new(:strength, :dexterity, :endurance,
6
+ :intelligence, :education, :social_status) do
7
+ def self.roll(spec = '2d6')
8
+ self.new(*Array.new(6) { TravellerRPG.roll spec })
9
+ end
10
+
11
+ def self.empty
12
+ self.new(*Array.new(6) { 0 })
13
+ end
14
+
15
+ def boost(hsh)
16
+ hsh.each { |k,v| self[k] += v if self[k] }
17
+ self
18
+ end
19
+ end
20
+
21
+ Description = Struct.new(:name, :gender, :age,
22
+ :appearance, :plot, :temperament) do
23
+ def self.new_with_hash(hsh)
24
+ self.new(hsh[:name], hsh[:gender], hsh[:age],
25
+ hsh[:appearance], hsh[:plot], hsh[:temperament])
26
+ end
27
+
28
+ def merge(other)
29
+ other = self.class.new_with_hash(other) if other.is_a?(Hash)
30
+ self.class.new(other.name || self.name,
31
+ other.gender || self.gender,
32
+ other.age || self.age,
33
+ other.appearance || self.appearance,
34
+ other.plot || self.plot,
35
+ other.temperament || self.temperament)
36
+ end
37
+ end
38
+
39
+ def self.stats_dm(stat)
40
+ case stat
41
+ when 0 then -3
42
+ when 1..2 then -2
43
+ when 3..5 then -1
44
+ when 6..8 then 0
45
+ when 9..11 then 1
46
+ when 12..14 then 2
47
+ when 15..20 then 3
48
+ else
49
+ raise "unexpected stat: #{stat} (#{stat.class})"
50
+ end
51
+ end
52
+
53
+ attr_reader :desc, :stats, :homeworld, :skills, :stuff
54
+
55
+ def initialize(desc:, stats:, homeworld:,
56
+ skills: {}, stuff: {}, log: [])
57
+ @desc = desc
58
+ @stats = stats
59
+ @homeworld = homeworld
60
+ @skills = skills
61
+ @stuff = stuff
62
+ @log = log
63
+ self.birth
64
+ end
65
+
66
+ # gain background skills based on homeworld
67
+ def birth
68
+ return nil unless @log.empty?
69
+ self.log format("%s was born on %s (%s)",
70
+ @desc.name,
71
+ @homeworld.name,
72
+ @homeworld.traits.join(' '))
73
+ skill_count = 3 + self.class.stats_dm(@stats.education)
74
+ self.log format("Education %i qualifies for %i skills",
75
+ @stats.education, skill_count)
76
+ skill_choices = []
77
+
78
+ # choose skill_count skills
79
+ if @homeworld.skills.size <= skill_count
80
+ self.log format("Homeworld %s only has %i skills available",
81
+ @homeworld.name, @homeworld.skills.size)
82
+ skill_choices = @homeworld.skills
83
+ else
84
+ skill_count.times { |i|
85
+ available = @homeworld.skills - skill_choices
86
+ skill_choices << TravellerRPG.choose("Choose a skill:", *available)
87
+ }
88
+ end
89
+ skill_choices.each { |sym|
90
+ self.log "Acquired background skill: #{sym} 0"
91
+ @skills[sym] ||= 0
92
+ }
93
+ end
94
+
95
+ def add_stuff(benefits)
96
+ benefits.each { |sym, val|
97
+ self.log "Collecting #{sym} #{val}"
98
+ case @stuff[sym]
99
+ when Numeric, Array
100
+ self.log "Adding #{sym} #{val} to #{@stuff[sym]}"
101
+ @stuff[sym] += val
102
+ when NilClass
103
+ @stuff[sym] = val
104
+ else
105
+ raise("unexpected benefit: #{sym} #{val} (#{val.class})")
106
+ end
107
+ }
108
+ end
109
+
110
+ def log(msg = nil)
111
+ return @log unless msg
112
+ puts msg
113
+ @log << msg
114
+ end
115
+
116
+ def name
117
+ @desc.name
118
+ end
119
+
120
+ def age(years = nil)
121
+ years ? @desc.age += years : @desc.age
122
+ end
123
+
124
+ def skill_check?(skill, val = 0)
125
+ @skills[skill] and @skills[skill] >= val
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,20 @@
1
+ module TravellerRPG
2
+ module Data
3
+ PATH = File.expand_path(File.join(__dir__, '..', '..', 'data'))
4
+ raise "can't find #{PATH}" unless File.directory?(PATH)
5
+
6
+ def self.files
7
+ Dir[File.join(PATH, '*')]
8
+ end
9
+
10
+ def self.sample(filename)
11
+ path = File.join(PATH, filename)
12
+ raise "#{path} does not exist" unless File.exist?(path)
13
+ if filename.match %r{\.txt\z}
14
+ File.readlines(path).sample.chomp
15
+ else
16
+ raise "can't handle #{filename}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,63 @@
1
+ require 'traveller_rpg'
2
+ require 'traveller_rpg/data'
3
+ require 'traveller_rpg/character'
4
+ require 'traveller_rpg/homeworld'
5
+
6
+ module TravellerRPG
7
+ module Generator
8
+ def self.character(descr = {}, homeworld: nil)
9
+ homeworld ||= self.homeworld
10
+ Character.new(desc: self.desc.merge(descr),
11
+ stats: Character::Stats.roll,
12
+ homeworld: homeworld)
13
+ end
14
+
15
+ def self.homeworld(name = nil)
16
+ name ||= Data.sample('homeworlds.txt')
17
+ Homeworld.new(name)
18
+ end
19
+
20
+ def self.name(gender)
21
+ case gender.to_s.downcase
22
+ when 'm', 'male'
23
+ Data.sample('male_names.txt')
24
+ when 'f', 'female'
25
+ Data.sample('female_names.txt')
26
+ else
27
+ raise "unknown gender: #{gender}"
28
+ end
29
+ end
30
+
31
+ def self.gender
32
+ TravellerRPG.roll(dice: 1) > 3 ? 'M' : 'F'
33
+ end
34
+
35
+ def self.hair(tone: nil, body: nil, color: nil, length: nil)
36
+ tone ||= Data.sample('hair_tone.txt')
37
+ body ||= Data.sample('hair_body.txt')
38
+ color ||= Data.sample('hair_colors.txt')
39
+ length ||= Data.sample('hair_length.txt')
40
+ [tone, body, color, length].join(' ')
41
+ end
42
+
43
+ def self.appearance(hair: nil, skin: nil)
44
+ hair ||= self.hair
45
+ skin ||= Data.sample('skin_tones.txt')
46
+ "#{hair} hair with #{skin} skin"
47
+ end
48
+
49
+ def self.plot
50
+ Data.sample('plots.txt')
51
+ end
52
+
53
+ def self.temperament
54
+ Data.sample('temperaments.txt')
55
+ end
56
+
57
+ def self.desc
58
+ gender = self.gender
59
+ Character::Description.new(self.name(gender), gender, 18,
60
+ self.appearance, self.plot, self.temperament)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,51 @@
1
+ require 'traveller_rpg'
2
+
3
+ module TravellerRPG
4
+ class Homeworld
5
+ TRAITS = {
6
+ agricultural: :animals,
7
+ asteroid: :zero_g,
8
+ desert: :survival,
9
+ fluid_oceans: :seafarer,
10
+ garden: :animals,
11
+ high_technology: :computers,
12
+ high_population: :streetwise,
13
+ ice_capped: :vacc_suit,
14
+ industrial: :trade,
15
+ low_technology: :survival,
16
+ poor: :animals,
17
+ rich: :carouse,
18
+ water_world: :seafarer,
19
+ vacuum: :vacc_suit,
20
+ education: [:admin, :advocate, :art, :carouse, :comms,
21
+ :computer, :drive, :engineer, :language, :medic,
22
+ :physical_science, :life_science, :social_science,
23
+ :space_science, :trade],
24
+ }
25
+ TRAIT_MIN = 3
26
+ TRAIT_MAX = 6
27
+
28
+ attr_reader :name, :traits, :skills
29
+
30
+ def initialize(name, traits = [])
31
+ @name = name
32
+ if traits.size > self.class::TRAIT_MAX
33
+ warn "lots of world traits: #{traits}"
34
+ elsif traits.empty?
35
+ sample_num = rand(TRAIT_MAX - TRAIT_MIN + 1) + TRAIT_MIN
36
+ traits = self.class::TRAITS.keys.sample(sample_num)
37
+ end
38
+ @traits = traits
39
+ @skills = []
40
+ @traits.each { |trait|
41
+ skill = self.class::TRAITS.fetch(trait)
42
+ if skill.is_a?(Array)
43
+ skill.each { |sk| @skills << sk }
44
+ else
45
+ @skills << skill
46
+ end
47
+ }
48
+ @skills.uniq!
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,222 @@
1
+ module TravellerRPG
2
+ PLAYER_CHOICE = ENV['PLAYER_CHOICE']
3
+
4
+ ROLL_RGX = %r{
5
+ \A # starts with
6
+ (\d*) # 0 or more digits; dice count
7
+ [dD] # character d
8
+ (\d+) # 1 or more digits; face count
9
+ \z # end str
10
+ }x
11
+
12
+ def self.roll(str = nil, faces: 6, dice: 2, count: 1)
13
+ return self.roll_str(str) if str
14
+ rolln = -> (faces, dice) { Array.new(dice) { rand(faces) + 1 } }
15
+ (Array.new(count) { rolln.(faces, dice).sum }.sum.to_f / count).round
16
+ end
17
+
18
+ def self.roll_str(str)
19
+ matches = str.match(ROLL_RGX) or raise("bad roll: #{str}")
20
+ dice, faces = matches[1], matches[2]
21
+ self.roll(dice: dice.empty? ? 1 : dice.to_i, faces: faces.to_i)
22
+ end
23
+
24
+ def self.choose(msg, *args)
25
+ return self.player_choose(msg, *args) if PLAYER_CHOICE
26
+ puts msg + ' (' + args.join(' ') + ')'
27
+ choice = args.sample
28
+ puts "> #{choice}"
29
+ choice
30
+ end
31
+
32
+ def self.player_choose(msg, *args)
33
+ chosen = false
34
+ while !chosen
35
+ puts msg + ' (' + args.join(' ') + ')'
36
+ choice = self.prompt.to_s.downcase.to_sym
37
+ if args.include?(choice)
38
+ chosen = choice
39
+ else
40
+ puts "Try again.\n"
41
+ end
42
+ end
43
+ chosen
44
+ end
45
+
46
+ def self.player_prompt(msg = nil)
47
+ print msg + ' ' if msg
48
+ print '> '
49
+ $stdin.gets(chomp: true)
50
+ end
51
+
52
+ def self.career_class(str)
53
+ Object.const_get("TravellerRPG::#{str.split('::').last}")
54
+ end
55
+
56
+ # per http://www.traveller-srd.com/core-rules/skills/
57
+ SKILLS = {
58
+ admin: nil,
59
+ advocate: nil,
60
+
61
+ animals_group: nil,
62
+ animals_riding: nil,
63
+ animals_veterinary: nil,
64
+ animals_training: nil,
65
+ animals_farming: nil,
66
+
67
+ athletics_group: nil,
68
+ athletics_coordination: nil,
69
+ athletics_endurance: nil,
70
+ athletics_strength: nil,
71
+ athletics_flying: nil,
72
+
73
+ art_group: nil,
74
+ art_acting: nil,
75
+ art_dance: nil,
76
+ art_holography: nil,
77
+ art_instrument: nil,
78
+ art_sculpting: nil,
79
+ art_writing: nil,
80
+
81
+ astrogation: nil,
82
+ battle_dress: nil,
83
+ broker: nil,
84
+ carouse: nil,
85
+ comms: nil,
86
+ computers: nil,
87
+ deception: nil,
88
+ diplomat: nil,
89
+
90
+ drive_group: nil,
91
+ drive_hovercraft: nil,
92
+ drive_mole: nil,
93
+ drive_tracked: nil,
94
+ drive_walker: nil,
95
+ drive_wheeled: nil,
96
+
97
+ engineer_group: nil,
98
+ engineer_manoeuvre: nil,
99
+ engineer_jump_drive: nil,
100
+ engineer_electronics: nil,
101
+ engineer_life_support: nil,
102
+ engineer_power: nil,
103
+
104
+ explosives: nil,
105
+
106
+ flyer_group: nil,
107
+ flyer_airship: nil,
108
+ flyer_grav: nil,
109
+ flyer_rotor: nil,
110
+ flyer_wing: nil,
111
+
112
+ gambler: nil,
113
+
114
+ gunner_group: nil,
115
+ gunner_turrets: nil,
116
+ gunner_ortillery: nil,
117
+ gunner_screens: nil,
118
+ gunner_capital_weapons: nil,
119
+
120
+ gun_combat_group: nil,
121
+ gun_combat_slug_rifle: nil,
122
+ gun_combat_slug_pistol: nil,
123
+ gun_combat_shotgun: nil,
124
+ gun_combat_energy_rifle: nil,
125
+ gun_combat_energy_pistol: nil,
126
+
127
+ heavy_weapons_group: nil,
128
+ heavy_weapons_launchers: nil,
129
+ heavy_weapons_man_portable_artillery: nil,
130
+ heavy_weapons_field_artillery: nil,
131
+
132
+ investigate: nil,
133
+ jack_of_all_trades: nil,
134
+
135
+ language_group: nil,
136
+ language_anglic: nil,
137
+
138
+ leadership: nil,
139
+
140
+ life_sciences_group: nil,
141
+ life_sciences_biology: nil,
142
+ life_sciences_cybernetics: nil,
143
+ life_sciences_genetics: nil,
144
+ life_sciences_psionicology: nil,
145
+
146
+ mechanic: nil,
147
+ medic: nil,
148
+
149
+ melee_group: nil,
150
+ melee_unarmed_combat: nil,
151
+ melee_blade: nil,
152
+ melee_bludgeon: nil,
153
+ melee_natural_weapons: nil,
154
+
155
+ navigation: nil,
156
+ persuade: nil,
157
+
158
+ pilot_group: nil,
159
+ pilot_small_craft: nil,
160
+ pilot_spacecraft: nil,
161
+ pilot_capital_ships: nil,
162
+
163
+ physical_sciences_group: nil,
164
+ physical_sciences_physics: nil,
165
+ physical_sciences_chemistry: nil,
166
+ physical_sciences_electronics: nil,
167
+
168
+ recon: nil,
169
+ remote_operations: nil,
170
+
171
+ seafarer_group: nil,
172
+ seafarer_sail: nil,
173
+ seafarer_submarine: nil,
174
+ seafarer_ocean_ships: nil,
175
+ seafarer_motorboats: nil,
176
+
177
+ sensors: nil,
178
+
179
+ social_sciences_group: nil,
180
+ social_sciences_archeology: nil,
181
+ social_sciences_economics: nil,
182
+ social_sciences_history: nil,
183
+ social_sciences_linguistics: nil,
184
+ social_sciences_philosophy: nil,
185
+ social_sciences_psychology: nil,
186
+ social_sciences_sophontology: nil,
187
+
188
+ space_sciences_group: nil,
189
+ space_sciences_planetology: nil,
190
+ space_sciences_robotics: nil,
191
+ space_sciences_xenology: nil,
192
+
193
+ stealth: nil,
194
+ steward: nil,
195
+ streetwise: nil,
196
+ survival: nil,
197
+
198
+ tactics_group: nil,
199
+ tactics_military: nil,
200
+ tactics_ground: nil,
201
+
202
+ trade_group: nil,
203
+ trade_biologicals: nil,
204
+ trade_civil_engineering: nil,
205
+ trade_space_construction: nil,
206
+ trade_hydroponics: nil,
207
+ trade_polymers: nil,
208
+
209
+ vacc_suit: nil,
210
+ zero_g: nil,
211
+ }
212
+ end
213
+
214
+ # compatibility stuff
215
+
216
+ unless Comparable.method_defined?(:clamp)
217
+ module Comparable
218
+ def clamp(low, high)
219
+ [[self, low].max, high].min
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'traveller_rpg'
3
+ s.summary = 'Automating portions of Traveller RPG'
4
+ s.description = 'Mostly based on Traveller SRD'
5
+ s.authors = ["Rick Hull"]
6
+ s.homepage = 'https://github.com/rickhull/traveller_rpg'
7
+ s.license = 'LGPL-3.0'
8
+ s.files = [
9
+ 'traveller_rpg.gemspec',
10
+ 'VERSION',
11
+ 'Rakefile',
12
+ 'README.md',
13
+ 'lib/traveller_rpg.rb',
14
+ 'lib/traveller_rpg/career_path.rb',
15
+ 'lib/traveller_rpg/career.rb',
16
+ 'lib/traveller_rpg/careers.rb',
17
+ 'lib/traveller_rpg/character.rb',
18
+ 'lib/traveller_rpg/data.rb',
19
+ 'lib/traveller_rpg/generator.rb',
20
+ 'lib/traveller_rpg/homeworld.rb',
21
+ 'bin/chargen',
22
+ ]
23
+ s.executables = ['chargen']
24
+ s.add_development_dependency 'buildar', '~> 3'
25
+ s.add_development_dependency 'minitest', '~> 5'
26
+ s.required_ruby_version = '~> 2'
27
+
28
+ s.version = File.read(File.join(__dir__, 'VERSION')).chomp
29
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: traveller_rpg
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Rick Hull
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: buildar
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5'
41
+ description: Mostly based on Traveller SRD
42
+ email:
43
+ executables:
44
+ - chargen
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - README.md
49
+ - Rakefile
50
+ - VERSION
51
+ - bin/chargen
52
+ - lib/traveller_rpg.rb
53
+ - lib/traveller_rpg/career.rb
54
+ - lib/traveller_rpg/career_path.rb
55
+ - lib/traveller_rpg/careers.rb
56
+ - lib/traveller_rpg/character.rb
57
+ - lib/traveller_rpg/data.rb
58
+ - lib/traveller_rpg/generator.rb
59
+ - lib/traveller_rpg/homeworld.rb
60
+ - traveller_rpg.gemspec
61
+ homepage: https://github.com/rickhull/traveller_rpg
62
+ licenses:
63
+ - LGPL-3.0
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '2'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.6.14
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Automating portions of Traveller RPG
85
+ test_files: []