-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpropp.js
1148 lines (981 loc) · 42.6 KB
/
propp.js
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
// based on code found @ https://web.archive.org/web/20061112014356/http://www.brown.edu/Courses/FR0133/Fairytale_Generator/gen.html
//
// PROPPIAN FAIRY TALE GENERATOR v1.0
//
// Generator script: Nicole Wee
// Fairy tale content: Laura Tan & Celeste Lim
// Authored: April, 2001 for
// FR133: Fairy Tales and Culture
// Prof. Lewis Seifert
// Brown University
//
// for more information contact: fgen@brown.edu
// https://web.archive.org/web/20061112014356/http://www.brown.edu/Courses/FR0133/Fairytale_Generator
//
'use strict';
var _ = _ || require('underscore');
var nlp_compromise = nlp_compromise || require('nlp_compromise');
var nlp = nlp_compromise;
// include sugar for node; not if html - in-progress
// var sugar = sugar || require('sugar');
// sugar is required for the Tokenizer (work on removing this dependency)
if (typeof [].first !== 'function') {
require('sugar');
}
var Tokenizer = Tokenizer || require('./tokenizer.web.js');
var Cleaner = Cleaner || require('./cleaner');
var cleaner = new Cleaner(Tokenizer);
// http://blog.elliotjameschong.com/2012/10/10/underscore-js-deepclone-and-deepextend-mix-ins/
// in case it is not clear, deepClone clones everything that can JSON-ified
// that means properties NOT FUNCTIONS
_.mixin({ deepClone: function (o) {
try {
return JSON.parse(JSON.stringify(o));
} catch (ex) {
console.log(ex.message);
console.log(o);
return undefined;
}
}});
// exposed statically as storyGen.world, as well as with instances
var world = {};
// "enum", with numbers for comparison?
world.sizes = {
'miniscule': 0,
'tiny': 1,
'small': 2,
'medium-sized': 3,
'large': 4,
'enormous': 5
};
// in a description, 'medium' would be skipped
// eg, 'male child' (or boy), 'young man', 'man', 'middle-aged man', 'old man', 'ancient man'
world.ages = {
'child': 0,
'young' : 1,
'medium': 2,
'middle-aged': 3,
'old': 4,
'ancient': 5
};
world.aspect = {
good: 'good',
bad: 'bad'
};
world.gender = {
female: 'female',
male: 'male'
// ,neuter: 'neuter'
};
world.healthLevel = {
alive: 'alive',
sickly: 'sickly',
dead: 'dead'
};
// for interdiction (using two words for this...)
world.interdictionType = {
movement: 'movement',
action: 'action',
speak: 'speakwith'
};
// we should have departure/death
// parents/siblings
// maybe make it simple: departure/death, familymember
world.absentationType = {
departure: 'departure',
death: 'death'
};
// elder := parents, grand-parents
// family := siblings and all those people we list.
world.absentationPerson = {
elder: 'elder',
elders: 'elders',
parent: 'parent',
parents: 'parents',
sibling: 'sibling',
siblings: 'siblings',
family: 'family'
};
world.blankLine = '';
world.util = {};
world.func8subfuncs = {
'1' : 'kidnapping of person',
'2' : 'seizure of magical agent or helper',
'2b' : 'forcible seizure of magical helper',
'3' : 'pillaging or ruining of crops',
'4' : 'theft of daylight',
'5' : 'plundering in other forms',
'6' : 'bodily injury, maiming, mutilation',
'7' : 'causes sudden disappearance',
'7b' : 'bride is forgotten',
'8' : 'demand for delivery or enticement, abduction',
'9' : 'expulsion',
'10' : 'casting into body of water',
'11' : 'casting of a spell, transformation',
'12' : 'false substitution',
'13' : 'issues order to kill [requires proof]',
'14' : 'commits murder',
'15' : 'imprisonment, detention',
'16' : 'threat of forced matrimony',
'16b' : 'threat of forced matrimony between relatives',
'17' : 'threat of cannibalism',
'17b' : 'threat of cannibalism among relatives',
'18' : 'tormenting at night (visitation, vampirism)',
'19' : 'declaration of war'
};
// http://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
world.util.randomProperty = function(obj) {
var result;
var count = 0;
for (var prop in obj)
if (prop != 'id') {
if (Math.random() < 1/++count)
result = obj[prop];
}
return result;
};
// this capitalizes the first letter of a sentence, or the first word in each sentence
// in each paragraph or paragraphs
world.util.capitalize = function(str) {
if (!str) return null;
var lines = str.split('\n'),
clean = [];
for (var i = 0; i < lines.length; i++) {
clean.push(cleaner(lines[i]));
}
return clean.join('\n');
};
var storyGen = function(settings) {
// generates a random number
var random = function(limit){
var num = Math.floor(Math.random() * limit);
return num;
};
var pick = function(arr) {
return arr[Math.floor(Math.random() * arr.length)];
};
var pickRemove = function(arr) {
var index = Math.floor(Math.random() * arr.length);
return arr.splice(index,1)[0];
};
// return true or false
// 50-50 chance (unless override)
var coinflip = function(chance) {
if (!chance) { chance = 0.5; }
return (Math.random() < chance);
};
// this capitalizes the first letter of a sentence, or the first word in each sentence in a paragraph.
// it presumes all passed-in-lines to be a paragraph, and will collapse all line-breaks
// to create one paragraph
// var capitalize = function(str) {
// if (!str) return null;
// return cleaner(str);
// };
var capitalize = world.util.capitalize;
// ugh. capitalize is defined in propp.js
// bank { adjective: [], verbs: [] }
// TODO: why "ig" instead of "itemGenerator" ?
var ig = function(bank) {
var adjCount = random(4) + 1;
var adjs = [];
for (var i = 0; i < adjCount; i++) {
adjs.push(capitalize(pick(bank.itembank.adjectives)));
}
var thing = capitalize(pick(bank.itembank.nouns));
return adjs.join('-') + ' ' + thing;
};
// var itemGenerator = function() {
// // TODO since this is a depencency, IT SHOULD BE PASSED IN
// return ig(defaultbank.itembank);
// };
// this function creates things that are common to all template-worlds
var god = function(settings, wordbank, t) {
var theme = t;
var bank = _.deepClone(wordbank);
bank.itemGenerator = function() {
return ig(bank);
};
if (wordbank) {
// TODO: this is not enough!
// need to have these things as adjectives....
for (var i = 0; i < 10; i++) {
bank.magicalitem.push(bank.itemGenerator());
}
}
var cache = {};
// see also https://github.com/dariusk/corpora/blob/master/data/archetypes/character.json
var createCharacter = function(gndr, aspct) {
// TODO: what happens when we've used up everything in the bank?
// SOLUTION: don't worry about it: make the bank bigger than any of our templates
// for now...
gndr = gndr || world.util.randomProperty(world.gender);
aspct = aspct || world.util.randomProperty(world.aspect);
var adjs = (aspct === world.aspect.good ? bank.adjectives.personal : bank.adjectives.negative);
var descr = [pick(adjs), pick(adjs)];
var name = pickRemove(bank.names[gndr].concat(bank.names.neuter));
// alt: two adjs in front, two-ads in back: "Big Bad Joan" or "Joan the Big and Bad"
// WAY ALT: His Serene Highness Prince Robert Michael Nicolaus Georg Bassaraba von Brancovan von Badische, Marquis of Hermosilla, Count of Cabo St. Eugenio, Seventy-fourth Grand Master of the Knights of Malta,
// another model is "Brienne of Tarth" - have some sort of origin location
// we would NOT do this for "family", however.... so, some flag to pass in. :::sigh:::
var nick = (coinflip() ? name + ' the ' + capitalize(pick(descr)) : capitalize(pick(descr)) + ' ' + name);
return { name: name,
nickname: nick,
alignment: aspct,
gender: gndr,
possessions: [],
health: world.healthLevel.alive,
description: descr,
knows: [], // people known to character (identifier, not object-reference, so we don't get all circular)
id: uid.toString(),
getCharacter: getCharacter,
object: pronounobject(gndr),
pronoun: pronoun(gndr),
reflexivePronoun: reflexivePronoun(gndr),
possessive: possessive(gndr)
};
};
var createCharacters = function(gndr, aspct, count) {
var members = count || random(12) + 1;
var acqs = [];
for (var i = 0; i < members; i++) {
var g = (!gndr || gndr === 'random' ? world.util.randomProperty(world.gender) : gndr);
aspct = aspct || world.util.randomProperty(world.aspect);
acqs.push(createCharacter(g, aspct));
}
return acqs;
};
// they are not characters at this point.
var createFamily = function(gndr) {
// mother, father, siblings
// wife/husband, children
var family = {
mother: null,
father: null,
wife: null,
husband: null,
children: null,
siblings: null
};
// small percentage of the time lives alone. waaaah!
if (coinflip(0.1)) {
// lives alone!
} else {
// TODO: siblings
var maxSibs = (coinflip(0.3) ? 12 : 3);
var sibCount = random(maxSibs);
var boys = [];
var girls = [];
for (var i = 0; i < sibCount; i++) {
if (coinflip()) {
boys.push(pick(bank.names['male']));
} else {
girls.push(pick(bank.names['female']));
}
}
if (coinflip()) {
// mother, father, siblings
family.father = pick(bank.names['male']);
family.mother = pick(bank.names['female']);
family.siblings = { brothers: boys, sisters: girls };
} else {
// spouse, siblings
// GONNA BE TRADITIONAL HERE, SO FAR
if (gndr === world.gender.male) {
family.wife = pick(bank.names.female);
} else {
family.husband = pick(bank.names.male);
}
family.children = { boys: boys, girls: girls };
}
}
return family;
};
var place = function() {
// don't need to do a pick-remove
// but we should probably store all places
// and make sure we never duplicate....
return {
residence: pick(bank.residence),
vicinity: pick(bank.location),
nation: pick(bank.nation),
id: uid.toString('pl_') // prefix
};
};
var getPlace = function(id) {
var p;
if (cache.places && cache.places[id]) { p = cache.places[id]; }
return p;
};
var getCharacter = function(uid) {
if (typeof uid === 'object') { return uid; }
var c;
if (cache.characters[uid]) { c = cache.characters[uid]; }
return c;
};
// hero or villain
var createHero = function(gndr, aspct, item) {
// oooooh, we just want to ADD properties to the character
// so we d on't repeat the name, gender, posessions, etc....
var c = createCharacter(gndr, aspct);
var family = createCharacters(settings.peoplegender, aspct);
var acquaintances = createCharacters(settings.peoplegender, aspct);
c.family = [];
c.acquaintances = [];
c.knows = [];
for (var i = 0; i < family.length; i++) {
c.family.push(family[i].id);
c.knows.push(family[i].id);
cache.characters[family[i].id] = family[i];
}
// TODO: NOT THERE YET
// do a full-on createCharacter for each
// so that they are in the character bank
// and STILL have a simple family: []
// that can be used for reference
// except... how to pull from family array to relationship ?!?!!????
// UGH UGH UGH UGH UGH
//
// I just want to be able to say "lived alone" or "lived with mother, father and 7 brothers" or something
// and, of cours,e be able to make use of those people....
//
// c.family = createFamily(gndr);
for (i = 0; i < acquaintances.length; i++) {
c.acquaintances.push(acquaintances[i].id);
c.knows.push(acquaintances[i].id);
cache.characters[acquaintances[i].id] = acquaintances[i];
}
c.home = place();
c.location = c.home.residence;
c.introduced = false;
if (item) { c.possessions.push(item); }
return c;
};
var createMagicalHelper = function(g, aspct) {
var person = createCharacter(g, aspct);
person.name = capitalize(pick(bank.itembank.adjectives)) + ' ' + person.name;
return person;
};
// differs from here in that there are no acquaintances
// and only creates minions if a number is passed in
// and there is a fantastic form
var createVillain = function(g, aspct, item, minionCount) {
if (!minionCount) minionCount = 1;
var c = createCharacter(g, aspct);
c.family = [];
c.acquaintances = []; // leave blank
if (minionCount) {
c.family = createCharacters(settings.peoplegender, aspct, minionCount);
// TODO not in the character cache?
}
c.home = place();
c.location = c.home.residence;
c.introduced = false; // introduced in story
if (item) { c.possessions.push(item); }
c.form = (coinflip() ? 'human' : pick(bank.fantasticForm));
return c;
};
var createHome = function() {
return place();
};
var createFalsehero = function() {
var g = world.util.randomProperty(world.gender);
var c = createCharacter(g, world.aspect.bad);
return c;
};
var createMagicalitem = function() {
// pick REMOVE
// which means we run of of the durned things.
// why not just create them here AS NEEDED
var item = (bank.magicalitem && bank.magicalitem.length > 0 ? pickRemove(bank.magicalitem) : bank.itemGenerator());
return item;
};
var createPunished = function() {
return pick(bank.punish);
};
// hrm. we are now storing people as string-ids. OUCH
var getName = function(thing, property) {
if (property && typeof property === 'function' ) { property = property(); }
if (property && thing[property]) { return getCharacter(thing)[property]; }
// if thing starts with "id_" then it is a character reference
// or another sort of reference. hrm.
if (thing.indexOf('id_') == 0) { property = property || select('name', 'nickname'); }
var elem = (typeof thing === 'string' && thing.indexOf('id_') == -1 ? thing : getCharacter(thing)[property]);
return elem;
};
var possessive = function(gndr) {
// if character is passed in, reduce it to the target gender
if (gndr && gndr.gender) { gndr = gndr.gender; }
return (gndr === world.gender.male ? 'his' : (gndr === world.gender.female ? 'her' : 'its'));
};
// third-person
var pronounobject = function(gndr) {
// if character is passed in, reduce it to the target gender
if (gndr && gndr.gender) { gndr = gndr.gender; }
return (gndr === world.gender.male ? 'him' : (gndr === world.gender.female ? 'her' : 'them'));
};
var pronoun = function(gndr) {
// if character is passed in, reduce it to the target gender
if (gndr && gndr.gender) { gndr = gndr.gender; }
return (gndr === world.gender.male ? 'he' : (gndr === world.gender.female ? 'she' : 'it'));
};
// none of these deal with plurals
var reflexivePronoun = function(gndr) {
// if character is passed in, reduce it to the target gender
if (gndr && gndr.gender) { gndr = gndr.gender; }
return (gndr === world.gender.male ? 'himself' : (gndr === world.gender.female ? 'herself' : 'itself'));
};
// calculate the speaking tone
// VERY PRELIMINARY
var tone = function(p1, p2) {
// comparison of alignment or other characteristics
// friendly - good <-> good | bad <-> bad
// love - hero <->bride[groom]
// dislike
// hatred
// mourning - person.alive -> person.dead
// the latter suggests a post-mortem tone in the other direction. let's leave that for now?
var t;
if (p1 && p1.alignment && p2 && p2.alignment) {
t = ((p1.alignment == p2.alignment) ? 'good' : 'bad');
}
return t;
};
/**
TODO: so, this uses so much language that is not template-specific.
it SHOULD be template-related
-----
not really sure where this is going.
see if they are friendly? talk about the location? posessions?
pass in an intent?
adjectives are completely random. SO IT GOES.
TODO: get a better sense of sentiment
the relationship of speaker to speaker
like, awe, fondness, sadness, dislike, hatred
if nobody to speak to, use an interjection, or a comment on the current location
if other person is dead, talk in memorium (how so, what to remember?)
if all is well, could talk about locale, descriptions, homes, posessions
would we want to _transfer_ a posession during conversation?
that could be interesting....
characters known to each other?
if not, they should introduce
one could know another
so, each char has a "knows" list of characters.
ugh. this will get recursive, and can't be serialized
should just be names, I suppose. more overhead, but serialization is kept. yes?
see also https://github.com/dariusk/corpora/blob/master/data/words/proverbs.json
dialogue
**/
/**
* conversation between p1 and p2, minimally sentiment-aware
* if p2 is not defined, p1 talks alound in interjections.
**/
var converse = function(p1, p2) {
var c = [];
// "Don't go bragging like that!" says a rich merchant
// after somebody says something _about themselves_
// "Why do you call me?"
// "I didnt call you" replies the old man. "I don't even know who you are."
var t = tone(p1, p2);
var p1n = coinflip() ? p1.name : p1.nickname;
var p2n = '';
// TODO: push into template vocabs
var says = '{{<%= select("said", "remarked", "noted", "mused", "exclaimed", "ejected", "rumbled", "muttered") %>}}';
var reply = '{{<%= select("replied", "responded", "retorted", "volleyed", "returned", "muttered") %>}}';
if (p1 && p2) {
p2n = coinflip() ? p2.name : p2.nickname;
// TODO: this should probably be handled by the calling code
// AS WHO KNOWS
// but for now.....
// c.push('{{P1N}} {{bumped}} into {{P2N}}.');
// TODO: still pretty repetitive
// and the punctuation is funky
// not... interesting enough.
c.push('"{{GG}} {{P1N}}" {{SAY}} {{P2N}}.', world.blankLine);
c.push('"{{GG}} {{P2N}}" {{RPLY}} {{P1N}}.', world.blankLine);
c.push('"Well, you certainly are ' + p1.description[0] + '," {{SAY}} {{P2N}}.', world.blankLine);
c.push('"Yes, I am," {{conceded}} {{P1N}}. "But it\'s been said that I\'m also ' + p1.description[1] + '!"');
} else if (p1 && !p2) {
var punct = function() { return select('!', '.', '?'); };
c.push('"' + capitalize(pick(bank.interjections))
+ select('!', ',') + '" {{SAY}} {{P1N}}' + (coinflip() ? ' to nobody in particular.' : '.')
+ (coinflip() ? '' : ' "' + capitalize(pick(bank.interjections)) + punct() + '"'));
}
// apply "TONE" to converstaion -- ie, are they on good terms with each other
// this is extremely preliminary, and is currently either only "good" or "bad"
// not applicable to solo "conversations"
// so far.
if (t === 'bad') {
if (coinflip()) { c.push(world.blankLine, '"Don\'t go bragging like that!" says {{P2N}}'); }
}
var tmpl = c.join('\n').replace(/{{P1N}}/mg, p1n).replace(/{{P2N}}/mg, p2n)
.replace(/{{SAY}}/mg, says).replace(/{{RPLY}}/mg, reply)
.replace(/{{GG}}/mg, function() { return pick(bank.greetings[t]); });
return tmpl;
};
// battle between two people
// each of which could have family and acquaintances join in
// or does that need to be made explicit ???
// if victor is not passed in, the result could be random!
var battle = function(p1, p2, victor) {
// TODO: who knows!
// this really seems like something for the template, though. BLARG!
// weapons!
// the magical item that is passed from advisor to hero should be used
// and the villain needs a weapon
// aaaand, other things? Any other posession, I suppose? or just ones marked "weapon" ?
// that could be interesting. Objects in the following form:
// so, magical things fall into this category
var thing = {
item: 'egg', // name could be "Egg of Death" or "Death Egg" or "Primordial Magic Death Egg of the Borderlands" or something telse
//
name: 'Magical Egg of the Wong Foo the Elder',
description: ['shiny', 'round', 'egg-shaped', 'magical', 'filigreed', 'expensive'],
weapon: true, // or false
magic: true, // or false
// special powers? how would THAT work ????
origin: {
// if a person gave the object, or it was found ?
person: null,
place: null
}
};
};
// list it out, using optional property for value
// awkward construction....
var list = function(arr, property) {
var lst = '';
if (arr.length > 0) {
if (arr.length === 1) {
lst = getName(arr[0], property);
}
else if (arr.length === 2) {
lst = getName(arr[0], property) + ' and ' + getName(arr[1], property);
} else {
for (var i = 0; i < arr.length - 1; i++) {
lst += getName(arr[i], property) + ', ';
}
lst += 'and ' + getName(arr[arr.length - 1], property);
}
}
return lst;
};
var or = function(f1, f2) {
return (coinflip() ? f1() : f2() );
};
// select one of any number of arguments
var select = function() {
return pick(arguments);
};
var dump = function(thing) {
var target = (thing ? thing : cache);
return JSON.stringify(target, null, '\t');
};
// TODO: optionally pass in.. anything?
// narrator, to start with.
var init = function() {
// I don't think this is an issue anymore....
if (!bank) { return; }
try {
cache.characters = settings.characters || {}; // this holds all the people, elsewhere, use a uid to reference individuals
cache.places = {}; // holds uid of all places (in-progress)
if (!settings.herogender || settings.herogender === 'random') { settings.herogender = world.util.randomProperty(world.gender); }
if (!settings.villaingender || settings.villaingender === 'random') { settings.villaingender = world.util.randomProperty(world.gender); }
if (!settings.peoplegender || settings.peoplegender === 'random') { settings.peoplegender = world.util.randomProperty(world.gender); }
if (settings.narrator) { cache.narrator = settings.narrator; }
// TODO: alternately pass in an aspect, so the hero (main character) is an anti-hero, and the villain is a good person!
// maybe protagonist/antagonist and the main chars?
// and hero/villain is a particular instantiation?
// so that the protag is here, with antag as villain, OR protag as villain with antag as hero
// and the other two combos. What would THAT do?
cache.hero = settings.hero || createHero(settings.herogender, world.aspect.good);
cache.villain = settings.villain || createVillain(settings.villaingender, world.aspect.bad, createMagicalitem(), 2);
// TODO: magical item starts as a posession of the advisor, no?
// NO: it could just be... lying about.
// TODO: advisor gender could be a setting, so we can all-female stories.
cache.advisor = settings.advisor || createCharacter(null, world.aspect.good);
cache.advisor.introduced = false; // ugh.
cache.magicalitem = settings.magicalitem || createMagicalitem();
cache.magicalhelper = settings.magicalhelper || createMagicalHelper(null, world.aspect.good);
cache.punished = settings.punished || createPunished();
cache.task = pick(bank.task);
// AAARGH. doesn't work...
// cache.victim = settings.victim || getCharacter(pick(cache.hero.family.concat(cache.hero)));
cache.ascension = pick(bank.ascension);
cache.marries = pick(bank.marries);
// if there's nobody in the family, there can't be a falsehero!
// also, does not have to be viallain family
// this is just "current implementation"
// could be hero's family, villain, or the villain
cache.falsehero = settings.falsehero || pick(cache.villain.family);
} catch(ex) {
// the last 3 items are non-standard.....
var msg = ex.name + ' : ' + ex.message;
if (ex.lineNumber && ex.columnNumber && ex.stack) {
msg += ' line: ' + ex.lineNumber + ' col: ' + ex.columnNumber + '\n'
+ ex.stack;
}
console.log(msg);
}
}(settings);
// god return
return {
init: init,
advisor: cache.advisor,
falsehero: cache.falsehero,
hero: cache.hero,
villain: cache.villain,
createMagicalitem: createMagicalitem,
// magicalitem: cache.magicalitem, // this remains the same EACH LOOP
// either we give a new magicalitem, or we skip the donation sequence
magicalhelper: cache.magicalhelper,
task: cache.task,
victim: cache.victim,
ascension: cache.ascension,
marriage: cache.marries,
converse: converse,
tone: tone,
theme: theme,
cache: cache, // hunh. exposing this?????
or: or,
list: list,
possessive: possessive,
pronounobject: pronounobject,
pronoun: pronoun,
// ooop. what's the difference?!!!
reflexivePronoun: reflexivePronoun,
pick: pick,
select: select,
coinflip: coinflip,
capitalize: capitalize,
randomProperty: world.util.randomProperty,
dump: dump,
interdictionType: world.interdictionType,
location: place,
wordbank: bank,
nlp: nlp,
createVillain: createVillain,
createHero: createHero,
punished: function() { return pick(bank.punish); },
getCharacter: getCharacter
};
};
// populate template
// which may contain multiple sentences.
var sentence = function(func, helper, params) {
var f = '';
var isFunc = (typeof func === 'function');
if (func && func.active || func && isFunc) {
if (isFunc) {
// outro and other special methods
f = func(helper);
} else if (func.exec) {
// special exec method
f = func.exec.apply(null, [].concat(helper, params));
} else if(func.templates) {
// old-style array-of-templates-with-no-other-logic
f = func.templates[random(func.templates.length)];
} else {
return '';
}
var prior = f;
// console.log(f);
var vicn = '<%= coinflip() ? cache.victim.name : cache.victim.nickname %>';
var villn = '<%= coinflip() ? villain.nickname : villain.name %>';
var hn = '<%= coinflip() ? hero.nickname : hero.name %>';
var hh = '<%= hero.home.vicinity %>';
f = f.replace(/{{VICN}}/mg, vicn)
.replace(/{{HN}}/mg, hn)
.replace(/{{VN}}/mg, villn)
.replace(/{{HH}}/mg, hh);
var template = f;
var t = _.template(f);
f = t(helper);
if (f.indexOf('NaN') >= 0) {
console.log('prior: ' + prior + '\n\ntemplate: ' + template);
}
// TODO: attempt to parse sentences, and apply past tense....
// TROUBLE: this means we will also swap the tense of the embedded tales
// WAAAAH!
// hrm....
// how about... we check for the presence of a specific TAG, say '{{**}}''
// if present, remove it an swap case
// if not, pretend like nothing happened...
// I AM NOT HAPPY WITH THE RESULTS
f = f.replace(/{{\*\*}}/mg, ''); // so remove it
if (f.indexOf('{{**}}') >= 0) {
f = f.replace(/{{\*\*}}/mg, ''); // remove it
f = f.replace(/{{|}}/mg, ''); // remove old-style verb-tags
var tokenizer = new Tokenizer();
tokenizer.setEntry(f);
var sentences = tokenizer.getSentences();
var tensed = [];
for (var i = 0; i < sentences.length; i++) {
// console.log(sentences[i]);
if (this.settings.verbtense == 'past') {
tensed.push(capitalize(nlp.pos(sentences[i]).sentences[0].to_past().text()));
} else {
tensed.push(capitalize(nlp.pos(sentences[i]).sentences[0].to_present().text()));
}
}
// if there are paragraph or sentence breaks WE JUST LOST THEM
f = tensed.join(' ');
}
// handle non-template transforms.
var tag,
re = /\{{.*?\}}/g,
sentence = f;
while((tag = re.exec(f)) !== null) {
var verb = tag[0].replace(/\{|\}/g, '');
// because I've set up module && module.exports
// nlp thinks it's running in node.js....
// can make a global switch.....
var tense;
if (this.settings.verbtense == 'past') {
tense = nlp.verb(verb).to_past();
} else {
tense = nlp.verb(verb).to_present();
}
f = f.replace(tag[0], tense);
}
// exceptions
f = f.replace(/wered/mg, 'were').replace(/weres/mg, 'are').replace(/strided/mg, 'strode').replace(/wased/mgi, 'WAS');
f = capitalize(f); // condenses everything to a single giant paragraph. BLARG! (becuase: cleaner/tokenizer)
}
return f;
};
// this is for "old-style" functions. BLARG!
var enforceRules = function(story) {
if (story['func2'].active || story['func3'].active) {
story['func2'].active = true;
story['func3'].active = true;
}
// magical item must be given to hero in order to be used
// the obverse is not true (can be given but never used; pointless, but: whatever!)
if (story['func18'].active) {
story['func14'].active = true;
}
// if returning, must depart
// converse is not true
if (story['func21'].active) {
story['func11'].active = true;
}
return story;
};
// takes an array of function names
// returns the index of villainy (if found)
// or -1 (not found)
var findVillainy = function(storyFuncs) {
for (var i = 0; i < storyFuncs.length; i++) {
var f = settings.funcs[i];
var subFunc;
if (typeof f === 'object') {
subFunc = f[1];
f = f[0];
}
if (f === 'func8') return i;
}
return -1;
};
// generate the fairy tale
var generate = function(settings, theme){
try {
this.settings = settings;
var story = theme.templates(settings.functions, world, storyGen);
var restartVillainy = this.findVillainy(settings.funcs);
var tale = [];
this.universe = god(this.settings, theme.bank, theme);
for (var i = 0; i < settings.funcs.length; i++) {
var f = settings.funcs[i];
var params = null;
if (typeof f === 'object') {
// capture ALL OTHER POSSIBLE PARAMS
params = f.slice(1);
f = f[0];
}
// console.log(settings.funcs[i]);
// subFunc could be multiple params
// we need to flatten everything and use apply... maybe?
// var s2 = this.sentence.apply(null, [].concat(story[f], this.universe, subFunc));
var s2 = this.sentence(story[f], this.universe, params);
if (s2) { tale.push(s2); }
if (this.universe.hero.health === world.healthLevel.dead) { break; }
if (settings.bossfight && this.universe.villain.health == 'dead' && restartVillainy >= 0) {
if (this.universe.coinflip(0.8)) {
// we run out of names, because new villains have both family and acquaintances
// AND USE THEM ALL UP
this.universe.villain = this.universe.createVillain();
this.universe.cache.magicalitem = this.universe.createMagicalitem();
i = restartVillainy - 1; // one less, since it will be incremented on loop
} else {
restartVillainy = -1;
}
}
}
// TODO: unless prohibited, include outro by default
if (settings.conclusion) {
tale.push(this.sentence(story.outro, this.universe));
}
// TODO: get a new iterator
// it will be an array that is BUILT
// aaaand, let's presume that it has been passed in as part of SETTINGS
// for (var index in story) {
// // console.log(index);
// var s = this.sentence(story[index], storyGen.world);
// if (s) {
// tale.push(s);
// }
// }
var title = this.sentence(story.title, this.universe);
return {
title: title,
tale: tale.join('\n\n')
};
} catch(ex) {
// the last 3 items are non-standard.....
var msg = ex.name + ' : ' + ex.message;
if (ex.lineNumber && ex.columnNumber && ex.stack) {
msg += ' line: ' + ex.lineNumber + ' col: ' + ex.columnNumber + '\n'
+ ex.stack;
}
console.log(msg);
return msg;
}
};
// http://stackoverflow.com/questions/3231459/create-unique-id-with-javascript
var uid = new function (prefix) {
var u = 0;
prefix = prefix || 'id_';
this.toString = function () {
return prefix + u++;