-
Notifications
You must be signed in to change notification settings - Fork 0
/
yeetwords.rb
2589 lines (2495 loc) · 108 KB
/
yeetwords.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
### YeetWords
### a domain-specific language for text substitution, implemented in Ruby
### Copyright (C) 2021 Veronique Chellgren
###
### This program is free software: you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation, either version 3 of the License, or
### (at your option) any later version.
###
### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
### GNU General Public License for more details.
###
### You should have received a copy of the GNU General Public License
### along with this program. If not, see <https://www.gnu.org/licenses/>.
###
### author: Veronique (Vera) Chellgren
### https://github.com/verachell
###
### usage: ruby yeetwords.rb yourcodefile.txt
###
############################################################################
require 'set'
def lexer_puts(str)
# outputs the string to standard output, prefixed by a constant.
# this helps the user distinguish between messages from this program's
# lexer and parser versus fall-through messages from the ruby interpreter.
print "Y| "
puts str
end
def warning(severity, str, command = "", linenum = "", cominfo = "")
if command != "" then
str.concat(" in command #{command}")
if linenum != "" then
str.concat(" at line number #{linenum}")
if cominfo != "" then
str.concat(": #{cominfo}")
end
end
end
lexer_puts("#{severity.upcase} WARNING: #{str}")
end
def severe_warning
self.method(:warning).curry.("SEVERE")
end
def mild_warning
self.method(:warning).curry.("MILD")
end
def stop_with_error(str)
lexer_puts("STOPPING: SERIOUS ERROR. #{str}")
abort
end
def stop_with_general_command_error(desc, cmdname, linenum, comline, expected = "", actual = "")
startstr = "#{desc} in #{cmdname} in line number #{linenum.to_s}: #{comline}"
if expected != "" then
finstr = "\n Expected: #{expected}"
if actual != "" then
finstr = finstr.concat(" . You put: #{actual}")
end
else
finstr = ""
end
stop_with_error(startstr + finstr)
end
def stop_with_par_num_error
self.method(:stop_with_general_command_error).curry.("Incorrect number of parameters")
end
def stop_with_unknown_variable_error(varname)
self.method(:stop_with_general_command_error).curry.("Unknown variable name #{varname}")
end
def stop_with_syntax_error
self.method(:stop_with_general_command_error).curry.("Incorrect syntax")
end
def stop_with_par_value_error
self.method(:stop_with_general_command_error).curry.("Incorrect parameter value")
end
def arr_2_sentence(thearr, preceding = "")
# takes an array with 2 or more items and returns the items as a string
# listed in the following format: [preceding]__, [preceding]__ and [preceding]___
result = ""
if preceding != "" then
result = preceding + " "
end
thearr.each_with_index{|x, ind|
if ind < thearr.size - 2 then
tempstr = ", "
if preceding != "" then
tempstr.concat(preceding)
end
elsif ind == thearr.size - 2 then
tempstr = " and "
if preceding != "" then
tempstr.concat(preceding, " ")
end
else
tempstr = ""
end
result.concat(x, tempstr)}
return result
end
def select_random(unique, howmany, arr, exclude = [])
# Given an array and a number of desired items designated by the howmany
# parameter, returns an array with
# a random selection of items from the original array.
# In the situation where a larger number of unique items are requested
# than exist in the queried array, a smaller array will be returned than
# what was requested (this may be an empty array).
#
# This function also works with hashes as the input and will return a hash
# BUT the hash function MUST only be called with unique = true, otherwise
# may wind up with less results than expected from due to the conversion
# of a non-unique array to a hash. Because of this, it emits a warning
# if run with unique = false and arr.class = Hash
if unique == false and arr.class == Hash then
# WARN_ID APPLE
severe_warning.call("This error should not come up, but if it does, please contact the programmer. When selecting random items from a catalog of items, non-uniques are specified. This does not work well with a catalog and may result in fewer items being returned than requested. The program is able to continue, however.")
end
if arr.class == Array then
sourcearr = arr.clone
else
sourcearr = arr.clone.to_a
end
if exclude.class == Array then
excluded = exclude
else
excluded = exclude.to_a
end
destarr = Array.new
get_randoms = lambda{|source, dest, totalnum|
if (source.size == 0) or (dest.size == totalnum)
return dest
else
max_ind = source.size - 1
# performance enhancement - shortcut when there is only 1 item
if max_ind == 0 then
r_item = source[0]
else
r_ind = rand(0..max_ind)
r_item = source[r_ind]
end
dest << r_item
if unique == true then
newsource = source - [r_item]
else
newsource = source
end
dest = get_randoms.call(newsource, dest, totalnum)
end
}
result = get_randoms.call((sourcearr - excluded), destarr, howmany)
if arr.class == Hash then
return result.to_h
else
return result
end
end
def select_random_unique
# parameters for .call (howmany, arr, exclude = [])
# Variant of select_random to return only unique items -
# see select_random for details.
self.method(:select_random).curry.(true)
end
def select_random_one
# parameters for .call (arr, exclude = [])
# Variant of select_random to return only one item - see
# select_random for details.
self.method(:select_random).curry.(false, 1)
end
def gender_set_exists?(curr_state, str)
# given a string and the current state, returns true if the gender
# in the string corresponds to a set of genders, otherwise returns false.
if curr_state["gender_info"].include?(str.strip.to_sym) then
true
else
false
end
end
def first_word(str)
# given a string (usually a command line string), returns the first
# word with whitespace stripped from both ends.
# Note that it has the limitation that if there is no space, it
# returns an empty string. It may be better rewritten to account for
# strings which are 1 word that do not contain a space. If doing a rewrite,
# be very careful to check functions that are calling it, as well
# as get_command. future_work
firstspace = str.strip.index(' ')
if firstspace == nil then
""
else
str[0..firstspace - 1].strip
end
end
def get_command(str)
# given a string that contains a command, returns the name of the command.
# Note that this function would be affected by any proposed changes
# to the function first_word. future_work
if first_word(str).empty? == false then
first_word(str).upcase
else
str.strip.upcase
end
end
def remove_first_word(str)
# given a string (usually a command line string), removes the first word
# and strips whitespace at both ends and returns the resultant string.
firstspace = str.index(' ')
if firstspace == nil then
""
else
str[firstspace..-1].strip
end
end
def valid_string_literal?(str)
# returns boolean value depending if string is surrounded by quotation marks.
# If str is to be stripped, it needs to be done prior to calling.
(str.class == String) and (str[0] == "\"") and (str[str.size - 1] == "\"")
end
def valid_int?(str)
# returns a boolean depending on whether the string when converted
# to integer, returns a value greater than or equal to zero
# This would be better renamed as valid_plus_int? future_work
(str.class == String) and str.to_i >= 0 and (str.match?(/[[:alpha:]]/) == false) and (str.match?(/[[:punct:]]/) == false)
end
def valid_num_range?(str)
# returns a boolean depending on whether the string contains a valid range.
# A valid range contains two dashes between two numbers; second number
# has to be equal to or greater than first number; first number has to be
# equal to or greater than 0.
result = false
if str.class == String then
if str.match?(/^[0-9]+--[0-9]+$/) then
nums = str.split("--").collect{|x| x.to_i}
if (nums[0] >= 0) and (nums[1] > nums[0]) then result = true end
end
end
return result
end
def contains_period?(str)
# given a string, returns a boolean based on whether the string contains a period.
str.strip.include?('.')
end
def num_range(str)
# returns an array of integer from a string containing a numerical range.
# The lower number of the range is first. If not a valid numerical range,
# an empty array is returned.
result = Array.new
if valid_num_range?(str) then
result = str.split("--").collect{|x| x.to_i}
end
end
def resolve_num_or_numrange(str)
# takes a string that represents either a numeric value or a random
# numeric range, and resolves it into a final number. If it does not
# correspond to a valid integer or a valid numeric range, 0 is returned.
# Note that 0 might be returned even if it's a valid integer or numeric
# range, since 0 is considered a valid number. Therefore 0 is not
# automatically an error message.
if valid_int?(str) then
str.to_i
elsif valid_num_range?(str) then
the_range = num_range(str)
rand(the_range[0]..the_range[1])
else
0
end
end
def valid_word_num?(str)
# Returns a boolean depending on whether the string str contains a valid
# word number, which would be a number with the letter W (case-insensitive)
# after
result = false
if str.dup.upcase.match?(/^[0-9]+W$/) and (str.match?(/[[:punct:]]/) == false) and str.to_i >= 0 then result = true end
return result
end
def valid_word_range?(str)
# Returns a boolean depending whether the string contains a valid word range
# a valid word range contains two dashes between a number and the letter "W"
# first number has to be >= 0, second has to be > first.
result = false
if str.class == String then
if str.dup.upcase.match?(/^[0-9]+W--[0-9]+W$/) then
nums = str.upcase.split("W--").collect{|x| x.to_i}
if (nums[0] >= 0) and (nums[1] > nums[0]) then result = true end
end
end
return result
end
def word_range(str)
# Returns an array of integer from a string containing a word range.
# The lower number of the word range is first (i.e. at the 0 index)
result = Array.new
if valid_word_range?(str) then
result = str.upcase.split("W--").collect{|x| x.to_i}
end
return result
end
def resolve_word_range(str)
# Given a string (str) that is known to contain a word range,
# returns a random integer number within the bounds of that range
range_arr = word_range(str)
result = nil
if range_arr.size == 2 then
result = rand(range_arr[0]..range_arr[1])
end
return result
end
def is_start?(str)
# Given a string (str) representing a user command line, returns
# a boolean that is true if the user entered a command
# that starts a block (e.g. "GEN", "LOOP", "DESC"), false otherwise.
if str.empty? == false then
str.upcase.start_with?("LOOP ") or (str.upcase == "LOOP") or str.upcase.start_with?("GEN ") or (str.upcase.strip == "GEN") or str.upcase.start_with?("DESC ") or (str.upcase == "DESC")
else
false
end
end
def is_end?(str)
# Given a string (str) representing a user command line, returns
# a boolean that is true if the user entered a command
# that ends a block (e.g. "GENEND", "LOOPEND", "DESCEND"), false otherwise.
if str.empty? == false then
str.upcase.start_with?("LOOPEND") or (str.upcase == "END") or str.upcase.start_with?("GENEND") or str.upcase.start_with?("DESCEND")
else
false
end
end
def word_count_str(thestring)
# Given a string (thestring), returns its word count as an integer
if thestring.strip.empty? == true then
result = 0
else
result = thestring.strip.squeeze(" ").count(" ") + 1
# exclude from counting markdown formatting characters or a dash as words
if thestring.include?("# ") then
result = result - 1
end
if thestring.include?( "> ") then
result = result -1
end
if thestring.include?("\n\n--- ") then
result = result -1
end
if thestring.include?(" \n# ") then
result = result - 1
end
if thestring.include?(" \n ") then
result = result - 1
end
end
return result
end
def display(curr_state, cmdhash)
# Displays the first N sentences (for positive values of N) on the screen
# or the last N sentences on the screen (for negative values of N), or all
# sentences on screen if no parameters are given. This function
# does not alter the story state in any way. Please note that formatting
# marks that are meaningful in markdown (e.g. blockquote mark > or the
# heading or subeading marks # or ## etc will be displayed as-is
# via the display command. Display should not be thought of as a direct
# reflection of the output as it would appear in markdown; instead it is
# mainly useful for diagnostic purposes for assessing the text of the story.
# remember that commands such as newpara and newline count as a
# sentence (a blank one) in the story.
par = remove_first_word(cmdhash[:comline]).strip
if par.split.size > 1 then
# ERR_ID ACORNSQUASH
stop_with_par_num_error.call("DISPLAY", cmdhash[:linenum], cmdhash[:comline], "0 or 1 numeric parameters", "#{par.split.size.to_s} parameters")
end
if par.split.size == 1 then
nsent = par.split[0].strip
if (nsent.match?(/^[\-]*[[:digit:]]+/) == false) or (nsent.to_i == 0) then
# ERR_ID AMARANTH
stop_with_par_value_error.call("DISPLAY", cmdhash[:linenum], cmdhash[:comline], "a non-zero numerical parameter", nsent)
end
n_int = nsent.to_i
end
puts "\n---BEGIN DISPLAY COMMAND #{cmdhash[:comline]} in line #{cmdhash[:linenum]}---"
if par.split.size == 0 then
puts curr_state["story_so_far"]
else
if n_int.abs > curr_state["story_so_far"].size then
# WARN_ID APRICOT
mild_warning.call("The number of sentences you specified is larger than the size of the story; displaying all sentences", "DISPLAY", cmdhash[:linenum], cmdhash[:comline])
puts curr_state["story_so_far"]
else
if n_int > 0 then
puts curr_state["story_so_far"][0, n_int]
else
puts curr_state["story_so_far"][n_int..-1]
end
end
end
puts "---END DISPLAY COMMAND #{cmdhash[:comline]} in line #{cmdhash[:linenum]}---"
end
def gen_parse(curr_state, cmdhash, gen_name, howmany)
# parses commands within the gen command. Takes the current state, a
# commandhash within the gen command (describing the desired variable
# assignment), the name of the gen, and the desired number
# of these gens, and assigns the user gen variables appropriately,
# returning the new state. This function is called by gen_user_structure
#
# A command consists of a variable name within the gen, then a number of
# how many items to return (this may include a random range),
# then the name of the word or sentence set to retrieve items from.
# The optional allunique parameter may be added, meaning that there will
# be no duplication of these in one gen compared to other gens in this set.
# For example, allunique is desirable for city names, since you
# presumably want each of your cities to have a different name.
# On the other hand, you may not need the allunique
# parameter if you are selecting a character's favorite color, since
# different characters may have the same favorite color.
# When requesting more than one item in a list in a city/character/gen,
# please note that multiples are always unique within that 1 city/
# chracter/gen, for example "friends 3 default_female_names" will always
# give 3 *different* names within that one gen. Adding the allunique
# parameter indicates that items are additionally to be unique
# across each of those gens, so in the above example adding allunique means
# that different people may not share a same friend name.
new_state = curr_state
# first figure out if the command is formatted correctly
# heck if cmdhash is a hash or an array
if cmdhash.class == Array then
# error - a looped structure was given instead of a single cmdhash
# ERR_ID SOYBEAN
lnum = new_state["curr_line"][:linenum].to_i + 1
stop_with_general_command_error("You cannot have nested structures in this location", "GEN", lnum.to_s, "you have a nested structure such as GEN or LOOP")
end
new_state["curr_line"] = cmdhash
allpars = cmdhash[:comline].strip
# we should expect between 3 and 4 parameters
if (allpars != "") and (allpars.upcase != "GENEND") and (allpars[0] != "#") and (is_end?(allpars.upcase) == false) then
# we have a non-blank and non-end line.
# Blank lines are allowed but we don't want to attempt to process them.
splitted_pars = allpars.split
if (splitted_pars.size > 4) or (splitted_pars.size < 3) then
# ERR_ID RUNNERBEAN
stop_with_par_num_error.call("GEN", cmdhash[:linenum], cmdhash[:comline], "3 - 4 parameters", splitted_pars.size.to_s)
end
# we have the right number of parameters
# get first word and remainder
var_name = first_word(allpars.downcase)
# here if var already exists under this gen, we want to know about it
info = access_value((gen_name + "." + var_name), new_state, :gen)
if info[:exists] == true then
# WARN_ID BANANA
mild_warning.call("Assigning variable #{var_name} - variable already exists. Data will be overwritten", "GEN", cmdhash[:linenum], cmdhash[:comline])
end
assignment_pars = remove_first_word(allpars)
num_gen = first_word(assignment_pars)
# check if the numerical parameter is valid
if (valid_int?(num_gen) == false) and (valid_num_range?(num_gen) == false) then
# ERR_ID ARRACACHA
stop_with_par_value_error.call("GEN", cmdhash[:linenum], cmdhash[:comline], "a valid number or numeric range in second argument", num_gen)
end
# need to resolve number before proceeding
amount_int = resolve_num_or_numrange(num_gen)
if amount_int <= 0 then
# ERR_ID SORREL
stop_with_par_value_error.call("GEN", cmdhash[:linenum], cmdhash[:comline], "a non-zero number", amount_int.to_s)
end
last_1or2_pars = remove_first_word(assignment_pars)
# need to check that the first of the 2 last pars is a valid set of
# words or sentences
last_1or2_split = last_1or2_pars.strip.split
if (last_1or2_split.size == 2) and (last_1or2_split[1].strip.upcase != "ALLUNIQUE") then
# invalid final parameter
# ERR_ID ARROWROOT
stop_with_par_value_error.call("GEN", cmdhash[:linenum], cmdhash[:comline], "ALLUNIQUE", last_1or2_split[1])
end
# at this point we have 1 or 2 final parameters with the second being
# ALLUNIQUE
# check first of the last 2 pars
if access_value(last_1or2_split[0].strip.downcase, new_state)[:exists] == false then
# then those words or sentences do not exist
# ERR_ID ARTICHOKE
stop_with_unknown_variable_error(last_1or2_split[0]).call("GEN", cmdhash[:linenum], cmdhash[:comline])
end
if access_value(last_1or2_split[0].strip.downcase, new_state)[:unit_type] != :list then
# then the variable is of the wrong type
# ERR_ID SHALLOT
stop_with_general_command_error("Incompatible variable type for #{last_1or2_split[0]}", "GEN", cmdhash[:linenum], cmdhash[:comline], "list", access_value(last_1or2_split[0].strip.downcase, new_state)[:unit_type].to_s)
end
# now there is a variable name, an existing vocab, and a valid number
# requested to generate of these.
vocab_choices = access_value(last_1or2_split[0].strip.downcase, new_state)[:value]
to_exclude = []
howmany.times{|one_gen|
selected_items = select_random_unique.call(amount_int, vocab_choices, to_exclude)
new_state["user_gens"][gen_name][one_gen][var_name] = selected_items
if last_1or2_split.size == 2 then
# we know from earlier in this function that this means
# the ALLUNIQUE parameter is there
to_exclude.concat(selected_items)
end
}
end
return new_state
end
def gen_user_structure(curr_state, info_arr)
# Given an array of commands containing a user-defined GEN structure,
# generates the appropriate gen structure and returns the current state
# with the newly defined user gen in user_gens.
# If the desired variable was previously in existence, it will be erased
# and replaced by the new data here. In that situation, a warning will be
# emitted to STDOUT but the program will continue.
new_state = curr_state
# get parameters of the first line
pars = remove_first_word(info_arr[0][:comline]).strip
# get remaining commands after first line
remaining_commands = info_arr[1..-1]
# parse parameters of first line
if pars == "" or pars.strip.split.size < 1 then
# ERR_ID RUTABAGA
stop_with_par_num_error.call("GEN", curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
end
if pars.strip.split.size == 1 then
var_name = pars.strip.downcase
else
var_name = first_word(pars).strip.downcase
end
if access_value(var_name, curr_state, :gen)[:exists] == true then
# WARN_ID BLACKBERRY
mild_warning.call("Assigning variable #{var_name} - variable already exists. Data will be overwritten", "GEN", curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
end
if contains_period?(var_name) then
# ERR_ID ARUGULA
stop_with_general_command_error("User-specified variable name #{var_name} invalid because it contains a period", "GEN", curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
end
if pars.strip.split.size == 1 then
user_amount = "1"
else
user_amount = pars.strip.split[1]
end
if valid_int?(user_amount) or valid_num_range?(user_amount) then
amount = resolve_num_or_numrange(user_amount)
user_amount = amount.to_s
else
# ERR_ID ASPARAGUS
stop_with_syntax_error.call("GEN", curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
end
if amount <= 0 then
# ERR_ID SNOWPEA
stop_with_par_value_error.call("GEN", curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline], "greater than zero", amount.to_s)
end
new_state["user_gens"][var_name] = Array.new
new_state["curr_gens"][var_name] = 0
if pars.strip.split.size == 3 then
desired_gender = remove_first_word(remove_first_word(pars).strip).strip.downcase
else
desired_gender = ""
end
# at this point we have a variable name (existing or not) and a parameter of
# how many to generate.
# If the male/female/nonbinary/binary/human/robot/all options are given, then
# generate an appropriate name and pronouns for the requested gender(s).
# If this parameter is not given, then no gender-specific name and pronouns
# will be assigned to that specific user gen. For example, if generating
# cities instead of characters, the user would not be wanting
# gender pronouns and should therefore leave the gender parameter blank.
# So, if you are generating characters, you should specify a gender
# parameter (unless you plan to handle names and pronouns some different way)
# Gender pronoun options are hard-coded into the program so user cannot
# currently change these (except by changing the source code), but adding
# support for user-specified pronouns (e.g. he/they) is a feature that
# might be supported later future_work
name_list = Array.new
user_amount.to_i.times{|n|
if desired_gender != "" then
# something was specified in gender field, need to handle pronouns
# and first name. They come as name, heshe, hisher, himher.
if gender_set_exists?(new_state, desired_gender) == false then
# the desired gender set definition does not exist
# ERR_ID ADZUKIBEAN
stop_with_general_command_error("desired gender #{desired_gender} is not defined", "GEN", curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
else
# generate gender items
r_gender = select_random_one.(new_state["gender_info"][desired_gender.to_sym])[0]
new_state["user_gens"][var_name][n] = Hash.new
# when assigning names, exclude ones already existing in the gen
curr_name = select_random_one.(new_state["gender_info"][r_gender]["names"], name_list)
new_state["user_gens"][var_name][n]["name"] = curr_name
name_list << curr_name[0]
new_state["user_gens"][var_name][n]["heshe"] = [new_state["gender_info"][r_gender]["pronouns"]["heshe"]]
new_state["user_gens"][var_name][n]["hisher"] = [new_state["gender_info"][r_gender]["pronouns"]["hisher"]]
new_state["user_gens"][var_name][n]["himher"] = [new_state["gender_info"][r_gender]["pronouns"]["himher"]]
new_state["user_gens"][var_name][n]["manwoman"] = [new_state["gender_info"][r_gender]["pronouns"]["manwoman"]]
new_state["user_gens"][var_name][n]["waswere"] = [new_state["gender_info"][r_gender]["pronouns"]["waswere"]]
new_state["user_gens"][var_name][n]["isare"] = [new_state["gender_info"][r_gender]["pronouns"]["isare"]]
end
else
new_state["user_gens"][var_name][n] = Hash.new
end }
# now move on to items which do not relate to gender
new_state = remaining_commands.reduce(new_state){|changing_state, gencmdhash|
gen_parse(changing_state, gencmdhash, var_name, user_amount.to_i)}
return new_state
end
def store_desc(curr_state, info_arr)
# When given the current state and info_arr an array containing
# cmdhashes describing a code block (= a desc) including a desired
# variable name, stores the code block under that name and returns
# the updated state of the story. Desc names do not collide with
# other user variable names as they do not occupy the same space.
new_state = curr_state
# get parameters of the first line
pars = remove_first_word(info_arr[0][:comline]).strip
if pars.split.size != 1 then
# ERR_ID BRUSSELSPROUT
stop_with_par_num_error.call("DESC", info_arr[0][:linenum], info_arr[0][:comline], "one parameter only, the name of the variable for storing this desc", "#{pars.split.size.to_s} parameters")
end
# get remaining commands after first line
remaining_commands = info_arr[1..-2]
varname = pars.downcase
new_state["user_descs"][varname] = remaining_commands
return new_state
end
def call(curr_state, cmdhash)
# Takes the current state and a command hash describing the call of a
# desc, sends the desc to be executed, and returns the updated state.
pars = remove_first_word(cmdhash[:comline]).strip
if pars.split.size != 1 then
# ERR_ID CABBAGE
stop_with_par_num_error.call("CALL", cmdhash[:linenum], cmdhash[:comline], "one parameter only, the name of the desc to be called", "#{pars.split.size.to_s} parameters")
end
varname = pars.downcase
# switch this out to make proper variable call
desc_info = access_value(varname, curr_state, :desc)
if desc_info[:exists] == false then
# ERR_ID CHICORY
stop_with_general_command_error("Unable to call the desc #{varname} as this desc does not exist or was not defined prior to calling it", "CALL", cmdhash[:linenum], cmdhash[:comline])
end
if (desc_info[:unit_type] != :list) or (desc_info[:value] == nil) then
# ERR_ID SEQUOIA
stop_with_general_command_error("Unable to call the desc #{varname} as it cannot be found or is of an unexpected format", "CALL", cmdhash[:linenum], cmdhash[:comline])
end
new_state = loop_iterator(curr_state, desc_info[:value], 0, 1, :cycle)
return new_state
end
def word_count_arr(arr_of_string)
# The main word count function. Given an primary array of strings, it
# returns the number of words in that array of string.
if arr_of_string == nil then
0
else
arr_of_string.inject(0) {|tot, sentences| tot + word_count_str(sentences)}
end
end
def arrjoin(orig_arr, to_add, curr_state, command="WORDJOIN")
# When given an array of strings (orig_arr), will concatenate the
# string(s) from that in to_add to the ones in orig_arr, returning
# a new array containing the concatenated strings.
# to_add may be a string literal or a user variable of type :list.
# If to_add is a list variable of a different length to orig_arr, then
# the shorter array will repeat until it reaches the length of the longer
# array. Typically only called by assign_parse in :wordjoin mode.
if valid_string_literal?(to_add.strip) then
add_arr = [to_add.strip[1..-2]]
else
add_info = access_value(to_add.strip.downcase, curr_state)
if add_info[:exists] == false then
# ERR_ID SWEETPOTATO
stop_with_unknown_variable_error(to_add).call(command, curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline], "an existing variable of type list or a string literal", to_add)
end
if add_info[:unit_type] != :list then
# ERR_ID TOMATO
stop_with_general_command_error("you specified an incompatible variable type", command, curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline], "a variable of type list or a string literal", to_add)
end
add_arr = add_info[:value]
end
if (add_arr != [""]) and (add_arr.empty? == false) then
# we have something to add to the original array
if orig_arr.empty? == true then
final_arr = add_arr
else
# neither the original array nor the array to add are empty
add_arr_size = add_arr.size
orig_arr_size = orig_arr.size
final_arr = Array.new
if add_arr_size >= orig_arr_size then
add_arr.each_with_index{|item, ind|
final_arr << orig_arr[ind % orig_arr_size] + item}
else
orig_arr.each_with_index{|item, ind|
final_arr << item + add_arr[ind % add_arr_size]}
end
end
else
# empty data was added
final_arr = orig_arr
end
return final_arr
end
def convert_case(style, item)
# given a style of case-conversion (:upcase, :lowcase, :supcase, :slowcase)
# and a string (item), converts case of item as desired, and returns
# the case-converted string.
if item != "" then
case style
when :upcase
item.upcase
when :lowcase
item.downcase
when :supcase
if item.size == 1 then
item.upcase
else
item[0].upcase + item[1..-1]
end
when :slowcase
if item.size == 1 then
item.downcase
else
item[0].downcase + item[1..-1]
end
else
""
end
else
""
end
end
def op_calc(result_so_far, one_item, style, curr_state, op)
# This function returns the value of result_so_far after addition or
# subtraction. It evaluates the result of adding or subtracting one_item
# such as a string literal or variable from the result_so_far.
# Typically called by calc_plus_minus which evaluates the right hand
# side of an assignment statement. op may be "+" or "-"
command = get_command(curr_state["curr_line"][:comline])
final_result = result_so_far
if style == :list then
allowable_types = Set[:stringlit, :list]
elsif style == :catalog then
allowable_types = Set[:list, :catalog]
elsif style == :genall then
allowable_types = Set[:genall]
else
# ERR_ID BAMBOOSHOOT
stop_with_general_command_error("Type mismatch error when attempting to assign #{one_item} to a variable", command, curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
end
# first determine if pieces could match, then do the final math
if valid_string_literal?(one_item.strip) then
one_type = :stringlit
one_value = one_item.strip[1..-2]
else
# if not a literal, we need to look up to see if exists
lookup = access_value(one_item.strip, curr_state)
if lookup[:exists] == false then
# ERR_ID BEETROOT
stop_with_unknown_variable_error(one_item).call(command, curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
end
# if we are here then we have an existing variable
one_type = lookup[:unit_type]
one_value = lookup[:value]
end
# first check if the types are allowable
if allowable_types.member?(one_type) == false then
# ERR_ID BELLPEPPER
stop_with_general_command_error("Type mismatch error when attempting to add or subtract #{one_item} to a variable - they are not of compatible types", command, curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
end
if op == "+" then
# + operation for :list is defined as adding the array or literal
# to result_so_far and then applying uniq
if style == :list then
if one_value.class == String then
final_result = (result_so_far + [one_value]).uniq
else
final_result = (result_so_far + one_value).uniq
end
end
if style == :catalog then
if one_value.class == Array then
final_result[one_item.strip.downcase] = one_value
else
final_result = result_so_far.merge(one_value)
end
end
if style == :genall then
final_result = (result_so_far + one_value).uniq
end
elsif op == "-" then
# calc subtracting it, typically result_so_far minus one_item but
# depends on data structures. For :catalog, subtraction is based
# on keys only. No attempt is made to check whether a key present in
# both items has the same value or not.
# For :genall, subtraction assumes all items of the subtracted gen are
# the same in both. If even one thing is a bit different, it won't
# subtract. Thus, if subtracting, it's important for the user to
# identify the item being subtracted just before the subtraction
# (either that, or to not modify any of the item properties after
# its creation).
if style == :list then
if one_value.class == String then
final_result = (result_so_far - [one_value]).uniq
else
final_result = (result_so_far - one_value).uniq
end
end
if style == :catalog then
if one_value.class == Array then
final_result = result_so_far.reject{|key, value|
key == one_item.strip.downcase}
else
final_result = one_value.keys.reduce(result_so_far){|changing, onekey|
changing.reject{|key, value|
key == onekey}
}
end
end
if style == :genall then
final_result = (result_so_far - one_value).uniq
end
else
# ERR_ID BLACKEYEDPEA
stop_with_general_command_error("Please contact the programmer. Attempting to evaluate the variable #{one_item} without a + or - operation. This situation should theoretically not arise", command, curr_state["curr_line"][:linenum], curr_state["curr_line"][:comline])
end
return final_result
end
def calc_plus_minus(returnable_datatype, splitted_plus_item, style, curr_state, cmdhash)
# Returns the final result in the appropriate data structure for that
# assignment statement. Evaluates the right hand side of an assignment
# statement, which may contain both + and - operations.
# This function is typically called from assignment_value.
splitted_minus = splitted_plus_item.split(" - ")
# the first item of the splitted minus will be a plus, since we had already
# splitted on plus in the previous function
result = op_calc(returnable_datatype, splitted_minus[0], style, curr_state, "+")
if splitted_minus.size > 1 then
result = splitted_minus.drop(1).reduce(op_calc(returnable_datatype, splitted_minus[0], style, curr_state, "+")){|changing, item|
op_calc(changing, item, style, curr_state, "-")}
end
return result
end
def assignment_value(rhs_str, style, curr_state, cmdhash)
# Given the right hand side (of the '=') for an assignment command as
# a string (rhs_str), computes the value of that desired assignment.
# Returns that value as either an array or a hash, whichever is
# dictated by the assignment command.
# Typically designed to be called by assign_parse.
# style can be :list, :catalog, or :genall. cmdhash is only
# used for error reporting purposes. curr_state is for lookups, except
# in the case of gens where the pointer to the active item of a gen
# may be modified during a subtractive assignment, to continue to point
# to the same active item.
# values are computed from left to right.
command = get_command(cmdhash[:comline])
if style == :list then
result = Array.new
elsif style == :catalog then
result = Hash.new
elsif style == :genall then
result = Array.new
else
# ERR_ID BOKCHOY
stop_with_general_command_error("Type mismatch error between command and value", command, cmdhash[:linenum], cmdhash[:comline])
end
# get splitted pluses, split them on minus, and calc value
splitted_plus = rhs_str.split(" + ")
# for each of the above, need to split on minus
# if the splitted minus is 1 item, add it to result
# if it's more than one, add first, then subtract others
# if any item is not in allowable types (test every one for stringlit) then
# stop with error. Otherwise keep evaluating.
result = splitted_plus.reduce(result){|changing, arritem|
calc_plus_minus(changing, arritem, style, curr_state, cmdhash)}
return result
end
def assign_value(style, curr_state, target, value)
# Returns state. Given a target variable (target) which may or may not be
# present in curr_state, and a value, assigns the value to the target
# variable in curr_state, returning a new udpated state with the
# variable assigned.
# style parameters are as for assign_parse :list, :catalog, :genall,
# :wordjoin, :upcase, :lowcase, :supcase, or :slowcase
# future_work Decide whether this function could benefit from some
# tightening up, since the style parameter is barely used. Any proposed
# change may affect assign_parse feeding into this, and
# access_value that is called by this. access_value should also
# ideally be altered to allow the ability to assign a new variable via
# the nesting of the if/then statements inside it - ultimately that ability
# would affect the calls made here and in any other function calling
# access_value in :change mode.
new_state = curr_state
targetname = target.strip.downcase
info = access_value(targetname, curr_state)
if info[:exists] == true
# we need to update a current variable
new_state = access_value(targetname, curr_state, :all, :change, value)
else
# we need to assign a new variable
if style == :genall then
new_state["user_gens"][targetname] = value
else
new_state["user_vars"][targetname] = value
end
end
return new_state
end
def assign_parse(style, curr_state, cmdhash)
# Takes the command line of an assign statement, parses it,
# updates values as required, and returns the new state.
# style represents type of assignment, and is either :list, :catalog,
# :genall, :wordjoin, :upcase, :lowcase, :supcase, or :slowcase
new_state = curr_state.clone
allpars = remove_first_word(cmdhash[:comline])
command = get_command(cmdhash[:comline])
l_r = split_l_r(allpars)
if l_r[:error] != 0 then
# ERR_ID BROCCOLI
stop_with_general_command_error("Too few arguments given or ' = ' missing", command, cmdhash[:linenum], cmdhash[:comline])
else
# we at least have a left and right side to the equals sign
rhs = l_r[:r]
lhs = l_r[:l]
rsplit = rhs.split
lsplit = lhs.split
rvalue = nil
if style == :wordjoin then
# First check there is more than 1 parameter
# The concatenation evaluates l to r. Valid parameters are
# string literals and lists. Returns a list which it assigns
# to variables on left hand side.
rsplit_plus = rhs.split(" + ")
if rsplit_plus.size < 2 then
# ERR_ID SPRINGONION
stop_with_par_num_error.call(command, cmdhash[:linenum], cmdhash[:comline], "2 parameters on the right hand side", "#{rsplit_plus.size.to_s} parameters")
end
# rvalue will either contain 1 item (if all pars are string literals)
# or multiple items (if there is at least 1 list in pars)
rvalue = rsplit_plus.reduce(Array.new){|changing, arritem|
arrjoin(changing, arritem, curr_state)}
elsif (style == :upcase) or (style == :lowcase) or (style == :supcase) or (style == :slowcase) then
# this only works on lists and string literals
rvar = rhs.strip
if valid_string_literal?(rvar) then
to_change = [rvar.strip[1..-2]]
else
to_change_info = access_value(rvar.strip.downcase, curr_state)
if to_change_info[:exists] == false then
# ERR_ID TURNIP
stop_with_unknown_variable_error(rvar).call(command, cmdhash[:linenum], cmdhash[:comline])
end
if to_change_info[:unit_type] != :list then
# ERR_ID WASABI
stop_with_general_command_error("Incompatible type specified in variable #{rvar}", command, cmdhash[:linenum], cmdhash[:comline], "a variable of type list")
end
to_change = to_change_info[:value]
end
rvalue = to_change.map{|arritem| convert_case(style, arritem)}
elsif (rsplit.size == 2) and (valid_int?(rsplit[1].strip) or valid_num_range?(rsplit[1].strip)) then
# We have 2 parameters with the second one a number, which means we need
# to randomly select the appropriate number of things. num should
# be greater than zero.
possible_value = access_value(rsplit[0].strip, curr_state)
howmany = 0