glossa 1.0.0

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 (3) hide show
  1. checksums.yaml +7 -0
  2. data/glossa.rb +571 -0
  3. metadata +45 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 692941c14d218ff50b931d210bd75aa17f12e322
4
+ data.tar.gz: 5e421adb5e9e08d1bfb4187973665da6daff4000
5
+ SHA512:
6
+ metadata.gz: 41274201912b958cfd6c68ea4770c1d944298ddf17ab8beb516135bb7327100cc500dd6940c9e9f97fb100082dc8d904773b903ddc8f30dfb0763e272c10c625
7
+ data.tar.gz: 5df005c86110b66b589599b7ffe23513fadb3421d5724aa439b418bd898c27f52ba6571b4033cd13cbd5ea00842130d5b018fb5e4c9f02afb1c5d8a00ac00aef
data/glossa.rb ADDED
@@ -0,0 +1,571 @@
1
+ # TODO: Import and export as json methods for a language
2
+ module Glossa
3
+ DEFAULT_ORTHO = {
4
+ 'ʃ' => 'sh',
5
+ 'ʒ' => 'zh',
6
+ 'ʧ' => 'ch',
7
+ 'ʤ' => 'j',
8
+ 'ŋ' => 'ng',
9
+ 'j' => 'y',
10
+ 'x' => 'kh',
11
+ 'ɣ' => 'gh',
12
+ 'ʔ' => '‘',
13
+ 'A' => "á",
14
+ 'E' => "é",
15
+ 'I' => "í",
16
+ 'O' => "ó",
17
+ 'U' => "ú"
18
+ }
19
+
20
+ C_ORTH_SETS = [
21
+ {
22
+ :name => "Default",
23
+ :orth => {}
24
+ },
25
+ {
26
+ :name => "Slavic",
27
+ :orth => {
28
+ 'ʃ' => 'š',
29
+ 'ʒ' => 'ž',
30
+ 'ʧ' => 'č',
31
+ 'ʤ' => 'ǧ',
32
+ 'j' => 'j'
33
+ }
34
+ },
35
+ {
36
+ :name => "German",
37
+ :orth => {
38
+ 'ʃ' => 'sch',
39
+ 'ʒ' => 'zh',
40
+ 'ʧ' => 'tsch',
41
+ 'ʤ' => 'dz',
42
+ 'j' => 'j',
43
+ 'x' => 'ch'
44
+ }
45
+ },
46
+ {
47
+ :name => "French",
48
+ :orth => {
49
+ 'ʃ' => 'ch',
50
+ 'ʒ' => 'j',
51
+ 'ʧ' => 'tch',
52
+ 'ʤ' => 'dj',
53
+ 'x' => 'kh'
54
+ }
55
+ },
56
+ {
57
+ :name => "Chinese (pinyin)",
58
+ :orth => {
59
+ 'ʃ' => 'x',
60
+ 'ʧ' => 'q',
61
+ 'ʤ' => 'j',
62
+ }
63
+ }
64
+ ]
65
+
66
+ V_ORTH_SETS = [
67
+ {
68
+ :name => "Ácutes",
69
+ :orth => {}
70
+ },
71
+ {
72
+ :name => "Ümlauts",
73
+ :orth => {
74
+ "A" => "ä",
75
+ "E" => "ë",
76
+ "I" => "ï",
77
+ "O" => "ö",
78
+ "U" => "ü"
79
+ }
80
+ },
81
+ {
82
+ :name => "Welsh",
83
+ :orth => {
84
+ "A" => "â",
85
+ "E" => "ê",
86
+ "I" => "y",
87
+ "O" => "ô",
88
+ "U" => "w"
89
+ }
90
+ },
91
+ {
92
+ :name => "Diphthongs",
93
+ :orth => {
94
+ "A" => "au",
95
+ "E" => "ei",
96
+ "I" => "ie",
97
+ "O" => "ou",
98
+ "U" => "oo"
99
+ }
100
+ },
101
+ {
102
+ :name => "Doubles",
103
+ :orth => {
104
+ "A" => "aa",
105
+ "E" => "ee",
106
+ "I" => "ii",
107
+ "O" => "oo",
108
+ "U" => "uu"
109
+ }
110
+ }
111
+ ]
112
+
113
+ CON_SETS = [
114
+ {
115
+ :name => "Minimal",
116
+ :C => "ptkmnls"
117
+ },
118
+ {
119
+ :name => "English-ish",
120
+ :C => "ptkbdgmnlrsʃzʒʧ"
121
+ },
122
+ {
123
+ :name => "Pirahã (very simple)",
124
+ :C => "ptkmnh"
125
+ },
126
+ {
127
+ :name => "Hawaiian-ish",
128
+ :C => "hklmnpwʔ"
129
+ },
130
+ {
131
+ :name => "Greenlandic-ish",
132
+ :C => "ptkqvsgrmnŋlj"
133
+ },
134
+ {
135
+ :name => "Arabic-ish",
136
+ :C => "tksʃdbqɣxmnlrwj"
137
+ },
138
+ {
139
+ :name => "Arabic-lite",
140
+ :C => "tkdgmnsʃ"
141
+ },
142
+ {
143
+ :name => "English-lite",
144
+ :C => "ptkbdgmnszʒʧhjw"
145
+ }
146
+ ]
147
+
148
+ S_SETS = [
149
+ {
150
+ :name => "Just s",
151
+ :S => "s"
152
+ },
153
+ {
154
+ :name => "s ʃ",
155
+ :S => "sʃ"
156
+ },
157
+ {
158
+ :name => "s ʃ f",
159
+ :S => "sʃf"
160
+ }
161
+ ]
162
+
163
+ L_SETS = [
164
+ {
165
+ :name => "r l",
166
+ :L => "rl"
167
+ },
168
+ {
169
+ :name => "Just r",
170
+ :L => "r"
171
+ },
172
+ {
173
+ :name => "Just l",
174
+ :L => "l"
175
+ },
176
+ {
177
+ :name => "w j",
178
+ :L => "wj"
179
+ },
180
+ {
181
+ :name => "r l w j",
182
+ :L => "rlwj"
183
+ }
184
+ ]
185
+
186
+ F_SETS = [
187
+ {
188
+ :name => "m n",
189
+ :F => "mn"
190
+ },
191
+ {
192
+ :name => "s k",
193
+ :F => "sk"
194
+ },
195
+ {
196
+ :name => "m n ŋ",
197
+ :F => "mnŋ"
198
+ },
199
+ {
200
+ :name => "s ʃ z ʒ",
201
+ :F => "sʃzʒ"
202
+ }
203
+ ]
204
+
205
+ VOW_SETS = [
206
+ {
207
+ :name => "Standard 5-vowel",
208
+ :V => "aeiou"
209
+ },
210
+ {
211
+ :name => "3-vowel a i u",
212
+ :V => "aiu"
213
+ },
214
+ {
215
+ :name => "Extra A E I",
216
+ :V => "aeiouAEI"
217
+ },
218
+ {
219
+ :name => "Extra U",
220
+ :V => "aeiouU"
221
+ },
222
+ {
223
+ :name => "5-vowel a i u A I",
224
+ :V => "aiuAI"
225
+ },
226
+ {
227
+ :name => "3-vowel e o u",
228
+ :V => "eou"
229
+ },
230
+ {
231
+ :name => "Extra A O U",
232
+ :V => "aeiouAOU"
233
+ }
234
+ ]
235
+
236
+ SYLL_STRUCTS = [
237
+ "CVC",
238
+ "CVV?C",
239
+ "CVVC?", "CVC?", "CV", "VC", "CVF", "C?VC", "CVF?",
240
+ "CL?VC", "CL?VF", "S?CVC", "S?CVF", "S?CVC?",
241
+ "C?VF", "C?VC?", "C?VF?", "C?L?VC", "VC",
242
+ "CVL?C?", "C?VL?C", "C?VLC?"
243
+ ]
244
+
245
+ RESTRICT_SETS = [
246
+ {
247
+ :name => "None",
248
+ :res => []
249
+ },
250
+ {
251
+ :name => "Double sounds",
252
+ :res => [/(.)\1/]
253
+ },
254
+ {
255
+ :name => "Doubles and hard clusters",
256
+ :res => [/[sʃf][sʃ]/, /(.)\1/, /[rl][rl]/]
257
+ }
258
+ ]
259
+
260
+ class Language
261
+ attr_accessor :phonemes, :structure, :exponent, :restricts, :cortho, :vortho, :noortho, :nomorph, :nowordpool, :minsyll, :maxsyll, :morphemes, :words, :names, :genitive, :definitive, :joiner, :maxchar, :minchar
262
+
263
+ def initialize(random = false, options = nil)
264
+ if random
265
+ @phonemes = {
266
+ "C" => shuffle(choose(CON_SETS, 2)[:C]),
267
+ "V" => shuffle(choose(VOW_SETS, 2)[:V]),
268
+ "L" => shuffle(choose(L_SETS, 2)[:L]),
269
+ "S" => shuffle(choose(S_SETS, 2)[:S]),
270
+ "F" => shuffle(choose(F_SETS, 2)[:F])
271
+ }
272
+ @noortho = false
273
+ @nomorph = false
274
+ @nowordpool = false
275
+ @structure = choose(SYLL_STRUCTS)
276
+ @exponent = rand(1..3)
277
+ @restricts = RESTRICT_SETS[2][:res]
278
+ @cortho = choose(C_ORTH_SETS, 2)[:orth]
279
+ @vortho = choose(V_ORTH_SETS, 2)[:orth]
280
+ @morphemes = {}
281
+ @words = {}
282
+ @names = []
283
+ @joiner = choose(' -')
284
+ @maxchar = rand(10...15)
285
+ @minchar = rand(3...5)
286
+ @minsyll = rand(1...3)
287
+ @maxsyll = rand(@minsyll + 1...7)
288
+
289
+ if @structure.length < 3
290
+ @minsyll += 1;
291
+ end
292
+ else
293
+ options ||= {}
294
+ @phonemes = options[:phonemes] || {
295
+ "C" => "ptkmnls",
296
+ "V" => "aeiou",
297
+ "S" => "s",
298
+ "F" => "mn",
299
+ "L" => "rl"
300
+ }
301
+ @structure = options[:structure] || "CVC"
302
+ @exponent = options[:exponent] || 2
303
+ @restricts = options[:restricts] || []
304
+ @cortho = options[:cortho] || {}
305
+ @vortho = options[:vortho] || {}
306
+ @noortho = options[:noortho] || true
307
+ @nomorph = options[:nomorph] || true
308
+ @nowordpool = options[:nowordpool] || true
309
+ @minsyll = options[:minsyll] || 1
310
+ @maxsyll = options[:maxsyll] || 1
311
+ @morphemes = options[:morphemes] || {}
312
+ @words = options[:words] || {}
313
+ @names = options[:names] || []
314
+ @joiner = options[:joiner] || ' '
315
+ @maxchar = options[:maxchar] || 12
316
+ @minchar = options[:minchar] || 5
317
+ end
318
+
319
+ @genitive = get_morpheme('of')
320
+ @definitive = get_morpheme('the')
321
+ end
322
+
323
+ ##
324
+ # Takes an array and picks a semi-random element, with the first
325
+ # elements weighted more frequently the the last elements by using
326
+ # the power of a given exponent.
327
+ def choose(list, exponent = 1)
328
+ listIndex = ((rand ** exponent) * list.length).floor
329
+
330
+ list[listIndex]
331
+ end
332
+
333
+ ##
334
+ # This is already how rand works? Need to verify
335
+ def rand_range(lo, hi = nil)
336
+ if hi.nil?
337
+ hi = lo
338
+ lo = 0
339
+ end
340
+
341
+ (rand * (hi - lo) + lo).floor
342
+ end
343
+
344
+ ##
345
+ # Takes an array or string and shuffles it into a random order.
346
+ def shuffle(list)
347
+ is_string = list.is_a? String
348
+ l = is_string ? list.chars : list
349
+ new_list = l.dup
350
+
351
+ i = 0;
352
+ l.each do |item|
353
+ tmp = item
354
+ rand_index = rand(i)
355
+ new_list[i] = new_list[rand_index]
356
+ new_list[rand_index] = tmp
357
+ i += 1;
358
+ end
359
+
360
+ if is_string
361
+ return new_list.join
362
+ else
363
+ return new_list
364
+ end
365
+ end
366
+
367
+ ##
368
+ # Takes an array of strings and an optional joiner string
369
+ # and concatenates them into a single string
370
+ def join(list, sep = '')
371
+ return '' if list.length == 0
372
+ first_word = list.shift
373
+ list.each do |item|
374
+ first_word << sep << item
375
+ end
376
+
377
+ first_word
378
+ end
379
+
380
+ ##
381
+ # Takes an array of phonetic syllables, and spells it using the languages orthography
382
+ def spell(syllables)
383
+ return syllables if self.noortho
384
+ s = syllables.chars
385
+ word = ''
386
+ s.each do |syllable|
387
+ if self.cortho[syllable]
388
+ word << self.cortho[syllable]
389
+ elsif self.vortho[syllable]
390
+ word << self.vortho[syllable]
391
+ elsif DEFAULT_ORTHO[syllable]
392
+ word << DEFAULT_ORTHO[syllable]
393
+ else
394
+ word << syllable
395
+ end
396
+ end
397
+ word
398
+ end
399
+
400
+ ##
401
+ # Creates a spelled (see the spell() method) syllable, according to
402
+ # the language's syllable structure. It does this by selecting a semi-random
403
+ # phonetic letter for the appropriate phoneme type in the structure, making sure
404
+ # that it doesn't conflict with a restricted pattern, and then spells the
405
+ # phonetic word according to the language's orthography.
406
+ def make_syllable
407
+ structure = self.structure.chars
408
+ while true
409
+ syll = ''
410
+ structure.each do |ptype|
411
+ # If the char is '?', skip with 50% chance to remove last character
412
+ if ptype == '?'
413
+ if rand < 0.5
414
+ syll = syll[0...syll.length - 1]
415
+ end
416
+ next
417
+ end
418
+
419
+ syll << choose(self.phonemes[ptype], self.exponent)
420
+ end
421
+ bad = false
422
+ self.restricts.each do |regex|
423
+ if regex =~ syll
424
+ bad = true
425
+ break
426
+ end
427
+ end
428
+ next if bad
429
+
430
+ return spell(syll)
431
+ end
432
+ end
433
+
434
+ ##
435
+ # The lowest common-denominator "word" that we will store for our language.
436
+ # Morphemes are the smallest unit of language that has a meaning.
437
+ # A "morpheme," in this sense, is a unique syllable (spelled according to our orthography)
438
+ # with a type (key). Whenever a morpheme is created, we store it in the class instance,
439
+ # so as to make sure we don't create duplicates. Morphemes comprise words.
440
+ def get_morpheme(key = '')
441
+ return make_syllable if self.nomorph
442
+
443
+ # Use the morphemes we've already made for this kind of word if possible
444
+ list = self.morphemes[key] || []
445
+
446
+ # If a key is not specified, make 10 generic morphemes
447
+ # otherwise, just make one new one
448
+ extras = key == '' ? 10 : 1
449
+
450
+ while true
451
+ # As more morphemes are created, there is a
452
+ # diminishing chance that a new one will be created.
453
+ n = rand_range(list.length + extras)
454
+ return list[n] if list[n]
455
+
456
+ morph = make_syllable
457
+
458
+ # No duplicates!
459
+ bad = false
460
+ self.morphemes.each do |k|
461
+ next if self.morphemes[k].nil?
462
+ if self.morphemes[k].include? morph
463
+ bad = true
464
+ break
465
+ end
466
+ end
467
+ next if bad
468
+ list << morph
469
+ self.morphemes[key] = list
470
+
471
+ return morph
472
+ end
473
+ end
474
+
475
+ ##
476
+ # Given the min- and max-syllables for our language, create a new
477
+ # word out of a random number of morphemes.
478
+ def make_word(key)
479
+ num_sylls = rand_range(self.minsyll, self.maxsyll + 1)
480
+ word = ''
481
+ keys = []
482
+
483
+ # If a key is defined, then select one of the syllables
484
+ # to have a morpheme of that type.
485
+ keys[rand_range(num_sylls)] = key
486
+ num_sylls.times { |i| word << get_morpheme(keys[i]) }
487
+
488
+ word
489
+ end
490
+
491
+ ##
492
+ # This method has a chance to use an existing word, or create a new
493
+ # one of the type (key) specified using the make_word method. If a
494
+ # new word is created, it will make sure that it is not duplicating
495
+ # an existing word, and then add it to the list of stored words.
496
+ def get_word(key = '')
497
+ words = self.words[key] || []
498
+ extras = key == '' ? 2 : 3
499
+
500
+ while true
501
+ n = rand_range(words.length + extras)
502
+ existing_word = words[n]
503
+ return existing_word if existing_word
504
+
505
+ new_word = make_word(key)
506
+ bad = false
507
+ self.words.each do |word|
508
+ if word.include? new_word
509
+ bad = true
510
+ break
511
+ end
512
+ end
513
+ next if bad
514
+ words << new_word
515
+ self.words[key] = words
516
+
517
+ return new_word
518
+ end
519
+ end
520
+
521
+ ##
522
+ # A wrapper with some additional logic around the get_word method.
523
+ # make_name will create a name, using get_word (so the words created
524
+ # to make up the name will be saved by the specified key), and have
525
+ # a 50% chance to add an additional word, and potentially the genitive
526
+ # and definitive words. After checking to make sure that it's an ok size
527
+ # and isn't already used, it saves and returns the name
528
+ def make_name(key = '')
529
+ ## If you don't dup these, words will get concatenated onto them during the join process
530
+ genitive = self.genitive.dup
531
+ definitive = self.definitive.dup
532
+ puts self.definitive
533
+ while true
534
+ # 50% chance that the name will be a single word, 50% chance that it will be two words combined somehow
535
+ name = rand < 0.5 ? get_word(key) : nil
536
+ if name.nil?
537
+ # 60% chance that each word will use the same key as invoked
538
+ w1_key = rand < 0.6 ? key : ''
539
+ w1 = get_word(w1_key).capitalize
540
+ w2_key = rand < 0.6 ? key : ''
541
+ w2 = get_word(w2_key).capitalize
542
+
543
+ # 50% chance to be joined without the lang's genitive word
544
+ if rand > 0.5
545
+ name = join([w1, w2], self.joiner)
546
+ else
547
+ name = join([w1, genitive, w2], self.joiner)
548
+ end
549
+ end
550
+
551
+ # 10% to prefix with definitive
552
+ name = join([definitive, name], self.joiner) if rand < 0.1
553
+ puts self.definitive
554
+ next if (name.length < self.minchar) || (name.length > self.maxchar)
555
+
556
+ used = false
557
+ self.names.each do |lang_name|
558
+ if (name.include? lang_name) || (lang_name.include? name)
559
+ used = true
560
+ break
561
+ end
562
+ end
563
+
564
+ next if used
565
+
566
+ self.names << name.capitalize
567
+ return name.capitalize
568
+ end
569
+ end
570
+ end
571
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: glossa
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jake Franklin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: glossa is a tool for creating simple naming language generators (which
14
+ can in turn generate names)
15
+ email: jacob.d.franklin@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - glossa.rb
21
+ homepage: http://rubygems.org/gems/glossa
22
+ licenses:
23
+ - MIT
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.6.8
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: A random naming language generator generator
45
+ test_files: []