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}