traveller_rpg 0.0.0.4

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