001package squidpony;
002
003import squidpony.squidmath.*;
004
005import java.util.*;
006
007/**
008 * A class for generating random monster descriptions; can be subclassed to generate stats for a specific game. Use the
009 * nested Chimera class for most of the functionality here; MonsterGen is here so you can change the descriptors that
010 * monsters can be given (they are in public array fields). You can call randomizeAppearance or randomizePowers on a
011 * Chimera to draw from the list of descriptors in MonsterGen, or fuse two Chimera objects with the mix method in the
012 * Chimera class. Chimeras can be printed to a usable format with presentVisible or present; the former does not print
013 * special powers and is suitable for monsters being encountered, and the latter is more useful for texts in the game
014 * world that describe some monster.
015 * Created by Tommy Ettinger on 1/31/2016.
016 */
017public class MonsterGen {
018
019    public static StatefulRNG srng = new StatefulRNG();
020
021    public String[] components = new String[]{"head", "tail", "legs", "claws", "fangs", "eyes", "hooves", "beak",
022            "wings", "pseudopods", "snout", "carapace", "sting", "pincers", "fins", "shell"},
023            adjectives = new String[]{"hairy", "scaly", "feathered", "chitinous", "pulpy", "writhing", "horrid",
024            "fuzzy", "reptilian", "avian", "insectoid", "tentacled", "thorny", "angular", "curvaceous", "lean",
025            "metallic", "stony", "glassy", "gaunt", "obese", "ill-proportioned", "sickly", "asymmetrical", "muscular"},
026    powerAdjectives = new String[]{"fire-breathing", "electrified", "frigid", "toxic", "noxious", "nimble",
027            "brutish", "bloodthirsty", "furious", "reflective", "regenerating", "earth-shaking", "thunderous",
028            "screeching", "all-seeing", "semi-corporeal", "vampiric", "skulking", "terrifying", "undead", "mechanical",
029            "angelic", "plant-like", "fungal", "contagious", "graceful", "malevolent", "gigantic", "wailing"},
030    powerPhrases = new String[]{"can evoke foul magic", "can petrify with its gaze", "hurts your eyes to look at",
031            "can spit venom", "can cast arcane spells", "can call on divine power", "embodies the wilderness",
032            "hates all other species", "constantly drools acid", "whispers maddening secrets in forgotten tongues",
033            "shudders between impossible dimensions", "withers any life around it", "revels in pain"};
034
035    /**
036     * A creature that can be mixed with other Chimeras or given additional descriptors, then printed in a usable format
037     * for game text.
038     */
039    public static class Chimera
040    {
041        public OrderedMap<String, List<String>> parts;
042        public OrderedSet<String> unsaidAdjectives, wholeAdjectives, powerAdjectives, powerPhrases;
043        public String name, mainForm, unknown;
044
045        /**
046         * Copies an existing Chimera other into a new Chimera with potentially a different name.
047         * @param name the name to use for the Chimera this constructs
048         * @param other the existing Chimera to copy all fields but name from.
049         */
050        public Chimera(String name, Chimera other)
051        {
052            this.name = name;
053            unknown = other.unknown;
054            if(unknown != null)
055                mainForm = unknown;
056            else
057                mainForm = other.name;
058            parts = new OrderedMap<>(other.parts);
059            List<String> oldParts = new ArrayList<>(parts.remove(mainForm));
060            parts.put(name, oldParts);
061            unsaidAdjectives = new OrderedSet<>(other.unsaidAdjectives);
062            wholeAdjectives = new OrderedSet<>(other.wholeAdjectives);
063            powerAdjectives = new OrderedSet<>(other.powerAdjectives);
064            powerPhrases = new OrderedSet<>(other.powerPhrases);
065        }
066
067        /**
068         * Constructs a Chimera given a name (typically all lower-case), null if the creature is familiar or a String if
069         * the creature's basic shape is likely to be unknown to players, and an array or vararg of String terms
070         * containing, usually, several groups of String elements separated by the literal string ";" . The first group
071         * in terms contains what body parts this creature has and could potentially grant to another creature if mixed;
072         * examples are "head", "legs", "claws", "wings", and "eyes". In the next group are the "unsaid" adjectives,
073         * which are not listed if unknown is false, but may be contributed to other creatures if mixed (mixing a horse
074         * with a snake may make the horse scaly, since "scaly" is an unsaid adjective for snakes). Next are adjectives
075         * that apply to the whole creature's appearance, which don't need to replicate the unsaid adjectives and are
076         * often added as a step to randomize a creature; this part is often empty and simply ends on the separator ";"
077         * . Next are the power adjectives, which are any special abilities a creature might have that aren't
078         * immediately visible, like "furious" or "toxic". Last are the power phrases, which follow a format like "can
079         * cast arcane spells", "embodies the wilderness", or "constantly drools acid"; it should be able to be put in a
080         * sentence after the word "that", like "a snake that can cast arcane spells".
081         * <br>
082         * The unknown argument determines if descriptions need to include basic properties like calling a Snake scaly
083         * (null in this case) or a Pestilence Fiend chitinous (no one knows what that creature is, so a String needs to
084         * be given so a player and player character that don't know its name can call it something, like "demon").
085         * <br>
086         * An example is {@code Chimera SNAKE = new Chimera("snake", null, "head", "tail", "fangs", "eyes", ";",
087         * "reptilian", "scaly", "lean", "curvaceous", ";", ";", "toxic");}
088         * @param name the name to refer to the creature by and its body parts by when mixed
089         * @param unknown true if the creature's basic shape is unlikely to be known by a player, false for animals and
090         *                possibly common mythological creatures like dragons
091         * @param terms an array or vararg of String elements, separated by ";" , see method documentation for details
092         */
093        public Chimera(String name, String unknown, String... terms)
094        {
095            this.name = name;
096            this.unknown = unknown;
097            if(unknown != null)
098                mainForm = unknown;
099            else
100                mainForm = name;
101            parts = new OrderedMap<>();
102            unsaidAdjectives = new OrderedSet<>();
103            wholeAdjectives = new OrderedSet<>();
104            powerAdjectives = new OrderedSet<>();
105            powerPhrases = new OrderedSet<>();
106            ArrayList<String> selfParts = new ArrayList<>();
107            int t = 0;
108            for (; t < terms.length; t++) {
109                if(";".equals(terms[t]))
110                {
111                    t++;
112                    break;
113                }
114                selfParts.add(terms[t]);
115            }
116            parts.put(name, selfParts);
117            for (; t < terms.length; t++) {
118                if (";".equals(terms[t])) {
119                    t++;
120                    break;
121                }
122                unsaidAdjectives.add(terms[t]);
123            }
124            for (; t < terms.length; t++) {
125                if (";".equals(terms[t])) {
126                    t++;
127                    break;
128                }
129                wholeAdjectives.add(terms[t]);
130            }
131            wholeAdjectives.removeAll(unsaidAdjectives);
132            for (; t < terms.length; t++) {
133                if (";".equals(terms[t])) {
134                    t++;
135                    break;
136                }
137                powerAdjectives.add(terms[t]);
138            }
139            for (; t < terms.length; t++) {
140                if (";".equals(terms[t])) {
141                    break;
142                }
143                powerPhrases.add(terms[t]);
144            }
145        }
146        /**
147         * Constructs a Chimera given a name (typically all lower-case), null if the creature is familiar or a String if
148         * the creature's basic shape is likely to be unknown to players, and several String Collection args for the
149         * different aspects of the Chimera. The first Collection contains what body parts this creature has and could
150         * potentially grant to another creature if mixed; examples are "head", "legs", "claws", "wings", and "eyes".
151         * The next Collection contains "unsaid" adjectives, which are not listed if unknown is false, but may be
152         * contributed to other creatures if mixed (mixing a horse with a snake may make the horse scaly, since "scaly"
153         * is an unsaid adjective for snakes). Next are adjectives that apply to the "whole" creature's appearance,
154         * which don't need to replicate the unsaid adjectives and are often added as a step to randomize a creature;
155         * this Collection is often empty. Next are the power adjectives, which are any special abilities a creature
156         * might have that aren't immediately visible, like "furious" or "toxic". Last are the power phrases, which
157         * follow a format like "can cast arcane spells", "embodies the wilderness", or "constantly drools acid"; it
158         * should be able to be put in a sentence after the word "that", like "a snake that can cast arcane spells".
159         * <br>
160         * The unknown argument determines if descriptions need to include basic properties like calling a Snake scaly
161         * (null in this case) or a Pestilence Fiend chitinous (no one knows what that creature is, so a String needs to
162         * be given so a player and player character that don't know its name can call it something, like "demon").
163         * <br>
164         * An example is {@code Chimera SNAKE = new Chimera("snake", null, "head", "tail", "fangs", "eyes", ";",
165         * "reptilian", "scaly", "lean", "curvaceous", ";", ";", "toxic");}
166         * @param name the name to refer to the creature by and its body parts by when mixed
167         * @param unknown true if the creature's basic shape is unlikely to be known by a player, false for animals and
168         *                possibly common mythological creatures like dragons
169         * @param parts the different body part nouns this creature can contribute to a creature when mixed
170         * @param unsaid appearance adjectives that don't need to be said if the creature is familiar
171         * @param whole appearance adjectives that apply to the whole creature
172         * @param powerAdj power adjectives like "furious" or "fire-breathing"
173         * @param powerPhr power phrases like "can cast arcane spells"
174         */
175        public Chimera(String name, String unknown, Collection<String> parts, Collection<String> unsaid,
176                       Collection<String> whole, Collection<String> powerAdj, Collection<String> powerPhr)
177        {
178            this.name = name;
179            this.unknown = unknown;
180            if(unknown != null)
181                mainForm = unknown;
182            else
183                mainForm = name;
184            this.parts = new OrderedMap<String, List<String>>();
185            unsaidAdjectives = new OrderedSet<String>(unsaid);
186            wholeAdjectives = new OrderedSet<String>(whole);
187            powerAdjectives = new OrderedSet<String>(powerAdj);
188            powerPhrases = new OrderedSet<String>(powerPhr);
189            ArrayList<String> selfParts = new ArrayList<String>(parts);
190            this.parts.put(name, selfParts);
191        }
192
193        /**
194         * Get a string description of this monster's appearance and powers.
195         * @param capitalize true if the description should start with a capital letter.
196         * @return a String description including both appearance and powers
197         */
198        public String present(boolean capitalize)
199        {
200            StringBuilder sb = new StringBuilder(), tmp = new StringBuilder();
201            if(capitalize)
202                sb.append('A');
203            else
204                sb.append('a');
205            int i = 0;
206            OrderedSet<String> allAdjectives = new OrderedSet<>(wholeAdjectives);
207            if(unknown != null)
208                allAdjectives.addAll(unsaidAdjectives);
209            allAdjectives.addAll(powerAdjectives);
210            for(String adj : allAdjectives)
211            {
212                tmp.append(adj);
213                if(++i < allAdjectives.size())
214                    tmp.append(',');
215                tmp.append(' ');
216            }
217            tmp.append(mainForm);
218            String ts = tmp.toString();
219            if(ts.matches("^[aeiouAEIOU].*"))
220                sb.append('n');
221            sb.append(' ').append(ts);
222            if(!(powerPhrases.isEmpty() && parts.size() == 1))
223                sb.append(' ');
224            if(parts.size() > 1)
225            {
226                sb.append("with the");
227                i = 1;
228                for(Map.Entry<String, List<String>> ent : parts.entrySet())
229                {
230                    if(name != null && name.equals(ent.getKey()))
231                        continue;
232                    if(ent.getValue().isEmpty())
233                        sb.append(" feel");
234                    else
235                    {
236                        int j = 1;
237                        for(String p : ent.getValue())
238                        {
239                            sb.append(' ').append(p);
240                            if(j++ < ent.getValue().size() && ent.getValue().size() > 2)
241                                sb.append(',');
242                            if(j == ent.getValue().size() && ent.getValue().size() >= 2)
243                                sb.append(" and");
244                        }
245                    }
246                    sb.append(" of a ").append(ent.getKey());
247
248
249                    if(i++ < parts.size() && parts.size() > 3)
250                        sb.append(',');
251                    if(i == parts.size() && parts.size() >= 3)
252                        sb.append(" and");
253                    sb.append(' ');
254                }
255            }
256
257            if(!powerPhrases.isEmpty())
258                sb.append("that");
259            i = 1;
260            for(String phr : powerPhrases)
261            {
262                sb.append(' ').append(phr);
263                if(i++ < powerPhrases.size() && powerPhrases.size() > 2)
264                    sb.append(',');
265                if(i == powerPhrases.size() && powerPhrases.size() >= 2)
266                    sb.append(" and");
267            }
268            return sb.toString();
269        }
270
271        /**
272         * Get a string description of this monster's appearance.
273         * @param capitalize true if the description should start with a capital letter.
274         * @return a String description including only the monster's appearance
275         */
276        public String presentVisible(boolean capitalize)
277        {
278            StringBuilder sb = new StringBuilder(), tmp = new StringBuilder();
279            if(capitalize)
280                sb.append('A');
281            else
282                sb.append('a');
283            int i = 0;
284
285            OrderedSet<String> allAdjectives = new OrderedSet<>(wholeAdjectives);
286            if(unknown != null)
287                allAdjectives.addAll(unsaidAdjectives);
288            for(String adj : allAdjectives)
289            {
290                tmp.append(adj);
291                if(++i < allAdjectives.size())
292                    tmp.append(',');
293                tmp.append(' ');
294            }
295            tmp.append(mainForm);
296            String ts = tmp.toString();
297            if(ts.matches("^[aeiouAEIOU].*"))
298                sb.append('n');
299            sb.append(' ').append(ts);
300            if(parts.size() > 1)
301            {
302                sb.append(" with the");
303                i = 1;
304                for(Map.Entry<String, List<String>> ent : parts.entrySet())
305                {
306                    if(name != null && name.equals(ent.getKey()))
307                        continue;
308                    if(ent.getValue().isEmpty())
309                        sb.append(" feel");
310                    else
311                    {
312                        int j = 1;
313                        for(String p : ent.getValue())
314                        {
315                            sb.append(' ').append(p);
316                            if(j++ < ent.getValue().size() && ent.getValue().size() > 2)
317                                sb.append(',');
318                            if(j == ent.getValue().size() && ent.getValue().size() >= 2)
319                                sb.append(" and");
320                        }
321                    }
322                    sb.append(" of a ").append(ent.getKey());
323
324
325                    if(i++ < parts.size() && parts.size() > 3)
326                        sb.append(',');
327                    if(i == parts.size() && parts.size() >= 3)
328                        sb.append(" and");
329                    sb.append(' ');
330                }
331            }
332
333            return sb.toString();
334        }
335
336        @Override
337        public String toString() {
338            return name;
339        }
340
341        /**
342         * Fuse two Chimera objects by some fraction of influence, using the given RNG and possibly renaming the
343         * creature. Does not modify the existing Chimera objects.
344         * @param rng the RNG to determine random factors
345         * @param newName the name to call the produced Chimera
346         * @param other the Chimera to mix with this one
347         * @param otherInfluence the fraction between 0.0 and 1.0 of descriptors from other to use
348         * @return a new Chimera mixing features from both inputs
349         */
350        public Chimera mix(RNG rng, String newName, Chimera other, double otherInfluence)
351        {
352            Chimera next = new Chimera(newName, this);
353            List<String> otherParts = other.parts.get(other.name),
354                    p2 = rng.randomPortion(otherParts, (int)Math.round(otherParts.size() * otherInfluence * 0.5));
355            next.parts.put(other.name, p2);
356            String[] unsaid = other.unsaidAdjectives.toArray(new String[0]),
357                    talentAdj = other.powerAdjectives.toArray(new String[0]),
358                    talentPhr = other.powerPhrases.toArray(new String[0]);
359            unsaid = portion(rng, unsaid, (int)Math.round(unsaid.length * otherInfluence));
360            talentAdj = portion(rng, talentAdj, (int)Math.round(talentAdj.length * otherInfluence));
361            talentPhr = portion(rng, talentPhr, (int)Math.round(talentPhr.length * otherInfluence));
362            Collections.addAll(next.wholeAdjectives, unsaid);
363            Collections.addAll(next.powerAdjectives, talentAdj);
364            Collections.addAll(next.powerPhrases, talentPhr);
365
366            return next;
367        }
368
369        /**
370         * Fuse two Chimera objects by some fraction of influence, using the default RNG and possibly renaming the
371         * creature. Does not modify the existing Chimera objects.
372         * @param newName the name to call the produced Chimera
373         * @param other the Chimera to mix with this one
374         * @param otherInfluence the fraction between 0.0 and 1.0 of descriptors from other to use
375         * @return a new Chimera mixing features from both inputs
376         */
377        public Chimera mix(String newName, Chimera other, double otherInfluence)
378        {
379            return mix(srng, newName, other, otherInfluence);
380        }
381    }
382    public static final Chimera SNAKE = new Chimera("snake", null, "head", "tail", "fangs", "eyes", ";",
383            "reptilian", "scaly", "lean", "curvaceous", ";",
384            ";",
385            "toxic"),
386            LION = new Chimera("lion", null, "head", "tail", "legs", "claws", "fangs", "eyes", ";",
387                    "hairy", "muscular", ";",
388                    ";",
389                    "furious"),
390            HORSE = new Chimera("horse", null, "head", "tail", "legs", "hooves", "eyes", ";",
391                    "fuzzy", "muscular", "lean", ";",
392                    ";",
393                    "nimble"),
394            HAWK = new Chimera("hawk", null, "head", "tail", "legs", "claws", "beak", "eyes", "wings", ";",
395                    "feathered", "avian", "lean", ";",
396                    ";",
397                    "screeching", "nimble"),
398            SHOGGOTH = new Chimera("shoggoth", "non-Euclidean ooze", "eyes", "fangs", "pseudopods", ";",
399                    "pulpy", "horrid", "tentacled", ";",
400                    ";",
401                    "terrifying", "regenerating", "semi-corporeal", ";",
402                    "shudders between impossible dimensions");
403
404    /**
405     * Constructs a MonsterGen with a random seed for the default RNG.
406     */
407    public MonsterGen()
408    {
409
410    }
411    /**
412     * Constructs a MonsterGen with the given seed for the default RNG.
413     */
414    public MonsterGen(long seed)
415    {
416        srng.setState(seed);
417    }
418    /**
419     * Constructs a MonsterGen with the given seed (hashing seed with CrossHash) for the default RNG.
420     */
421    public MonsterGen(String seed)
422    {
423        srng.setState(CrossHash.hash(seed));
424    }
425
426    /**
427     * Randomly add appearance descriptors to a copy of the Chimera creature. Produces a new Chimera, potentially with a
428     * different name, and adds the specified count of adjectives (if any are added that the creature already has, they
429     * are ignored, and this includes unsaid adjectives if the creature is known).
430     * @param rng the RNG to determine random factors
431     * @param creature the Chimera to add descriptors to
432     * @param newName the name to call the produced Chimera
433     * @param adjectiveCount the number of adjectives to add; may add less if some overlap
434     * @return a new Chimera with additional appearance descriptors
435     */
436    public Chimera randomizeAppearance(RNG rng, Chimera creature, String newName, int adjectiveCount)
437    {
438        Chimera next = new Chimera(newName, creature);
439        Collections.addAll(next.wholeAdjectives, portion(rng, adjectives, adjectiveCount));
440        next.wholeAdjectives.removeAll(next.unsaidAdjectives);
441        return next;
442    }
443
444    /**
445     * Randomly add appearance descriptors to a copy of the Chimera creature. Produces a new Chimera, potentially with a
446     * different name, and adds the specified count of adjectives (if any are added that the creature already has, they
447     * are ignored, and this includes unsaid adjectives if the creature is known).
448     * @param creature the Chimera to add descriptors to
449     * @param newName the name to call the produced Chimera
450     * @param adjectiveCount the number of adjectives to add; may add less if some overlap
451     * @return a new Chimera with additional appearance descriptors
452     */
453    public Chimera randomizeAppearance(Chimera creature, String newName, int adjectiveCount)
454    {
455        return randomizeAppearance(srng, creature, newName, adjectiveCount);
456    }
457
458    /**
459     * Randomly add power descriptors to a copy of the Chimera creature. Produces a new Chimera, potentially with a
460     * different name, and adds the specified total count of power adjectives and phrases (if any are added that the
461     * creature already has, they are ignored).
462     * @param rng the RNG to determine random factors
463     * @param creature the Chimera to add descriptors to
464     * @param newName the name to call the produced Chimera
465     * @param powerCount the number of adjectives to add; may add less if some overlap
466     * @return a new Chimera with additional power descriptors
467     */
468    public Chimera randomizePowers(RNG rng, Chimera creature, String newName, int powerCount)
469    {
470        Chimera next = new Chimera(newName, creature);
471        int adjs = rng.nextInt(powerCount + 1), phrs = powerCount - adjs;
472        Collections.addAll(next.powerAdjectives, portion(rng, powerAdjectives, adjs));
473        Collections.addAll(next.powerPhrases, portion(rng, powerPhrases, phrs));
474        return next;
475    }
476
477    /**
478     * Randomly add power descriptors to a copy of the Chimera creature. Produces a new Chimera, potentially with a
479     * different name, and adds the specified total count of power adjectives and phrases (if any are added that the
480     * creature already has, they are ignored).
481     * @param creature the Chimera to add descriptors to
482     * @param newName the name to call the produced Chimera
483     * @param powerCount the number of adjectives to add; may add less if some overlap
484     * @return a new Chimera with additional power descriptors
485     */
486    public Chimera randomizePowers(Chimera creature, String newName, int powerCount)
487    {
488        return randomizePowers(srng, creature, newName, powerCount);
489    }
490
491    /**
492     * Randomly add appearance and power descriptors to a new Chimera creature with random body part adjectives.
493     * Produces a new Chimera with the specified name, and adds the specified total count (detail) of appearance
494     * adjectives, power adjectives and phrases, and the same count (detail) of body parts.
495     * @param rng the RNG to determine random factors
496     * @param newName the name to call the produced Chimera
497     * @param detail the number of adjectives and phrases to add, also the number of body parts
498     * @return a new Chimera with random traits
499     */
500    public Chimera randomize(RNG rng, String newName, int detail)
501    {
502        ArrayList<String> ps = new ArrayList<String>();
503        Collections.addAll(ps, portion(rng, components, detail));
504        Chimera next = new Chimera(newName, "thing", ps, new ArrayList<String>(),
505                new ArrayList<String>(), new ArrayList<String>(), new ArrayList<String>());
506        if(detail > 0) {
507            int powerCount = rng.nextInt(detail), bodyCount = detail - powerCount;
508            int adjs = rng.nextInt(powerCount + 1), phrs = powerCount - adjs;
509
510            Collections.addAll(next.unsaidAdjectives, portion(rng, adjectives, bodyCount));
511            Collections.addAll(next.powerAdjectives, portion(rng, powerAdjectives, adjs));
512            Collections.addAll(next.powerPhrases, portion(rng, powerPhrases, phrs));
513        }
514        return next;
515    }
516
517    /**
518     * Randomly add appearance and power descriptors to a new Chimera creature with random body part adjectives.
519     * Produces a new Chimera with the specified name, and adds the specified total count (detail) of appearance
520     * adjectives, power adjectives and phrases, and the same count (detail) of body parts.
521     * @param newName the name to call the produced Chimera
522     * @param detail the number of adjectives and phrases to add, also the number of body parts
523     * @return a new Chimera with random traits
524     */
525    public Chimera randomize(String newName, int detail)
526    {
527        return randomize(srng, newName, detail);
528    }
529
530    /**
531     * Randomly add appearance and power descriptors to a new Chimera creature with random body part adjectives.
532     * Produces a new Chimera with a random name using FakeLanguageGen, and adds a total of 5 appearance adjectives,
533     * power adjectives and phrases, and 5 body parts.
534     * @return a new Chimera with random traits
535     */
536    public Chimera randomize()
537    {
538        return randomize(srng, randomName(srng), 5);
539    }
540
541    /**
542     * Gets a random name as a String using FakeLanguageGen.
543     * @param rng the RNG to use for random factors
544     * @return a String meant to be used as a creature name
545     */
546    public String randomName(RNG rng)
547    {
548        return FakeLanguageGen.FANTASY_NAME.word(rng, false, rng.between(2, 4));
549    }
550
551    /**
552     * Gets a random name as a String using FakeLanguageGen.
553     * @return a String meant to be used as a creature name
554     */
555    public String randomName()
556    {
557        return randomName(srng);
558    }
559
560    private static String[] portion(RNG rng, String[] source, int amount)
561    {
562        return rng.randomPortion(source, new String[Math.min(source.length, amount)]);
563    }
564
565}