001package squidpony; 002 003import squidpony.squidmath.*; 004 005import java.util.ArrayList; 006import java.util.Collection; 007 008import static squidpony.Thesaurus.*; 009 010/** 011 * A utility class to print (typically very large) numbers in a way that players can more-meaningfully tell them apart. 012 * It isn't that great for this task currently, but it can bi-directionally turn {@code long} values like 013 * -8798641734435409502 into {@code String}s like nwihyayeetyoruehyazuetro. The advantage here is that 014 * nwihyayeetyoruehyazuetro is very different from protoezlebauyauzlutoatra, even though the numbers they are made from 015 * are harder to distinguish (-8798641734435409502 vs. -8032477240987739423, when using the default seed). 016 * <br> 017 * The constructor optionally takes a seed that can greatly change the generated mnemonics, which may be useful if 018 * mnemonic strings produced for some purpose should only be decipherable by that program or that play of the game. If 019 * no seed is given, this acts as if the seed is 1. Only 256 possible 3-letter sections are used with any given seed, 020 * but 431 sections are possible (hand-selected to avoid the likelihood of producing possibly-vulgar words). Two 021 * different seeds may use mostly-different selections of "syllable" sections, though a not-very-small amount of overlap 022 * in potential generated mnemonic strings must occur between any two seeds. 023 * <br> 024 * Created by Tommy Ettinger on 1/24/2018. 025 */ 026public class Mnemonic { 027 private static final String baseTriplets = 028 "baibaublabyabeabeebeibeoblebrebwebyebiabiebioblibribwibyiboaboeboiboubrobuobyobuabuebuibrubwubyu" + 029 "daudradyadeadeedeodredwediodridwidyidoadoedoidoudroduodyoduadueduidrudwudyu" + 030 "haihauhmahrahyahwaheaheeheiheohmehrehwehyehiahiehiohmihrihwihyihmohrohuohyohuahuehuihmuhruhwuhyu" + 031 "jaijaujyajwajeajeejeijeojwejyejiajiejiojwijyijoajoejoijoujyo" + 032 "kaikaukrakyakeakeekeoklekrekyekiakiokrikwikyikoakoekoikouklokrokyokuokuakuekuikrukyu" + 033 "lailaulyalwalealeeleileolwelyelialieliolwilyiloaloeloiluolyolualuilwulyu" + 034 "maimaumlamramwamyameameemeimeomlemremwemyemiamiemiomlimrimwimyimoamoemoimoumlomromuomyomuamuemuimlumrumwumyu" + 035 "nainaunranwanyaneaneeneonrenwenyenianienionrinwinyinoanoenoinounronuonyonuanuenuinrunwunyu" + 036 "paipauplaprapwapyapleprepiapiepioplipripwipyipoapoepoiplopropuopyopluprupyu" + 037 "quaquequiquo" + 038 "rairauryareareereireoryeriarierioryiroaroeroirouryoruarueruiryu" + 039 "saisauskaslasmasnaswasyaseaseeseiseoskeslesmesneswesyesiasiesioskislismisniswisyisoasoesoisouskoslosmosnosuosyosuasuesuiskuslusmusnuswusyu" + 040 "taitautratsatwatyateateeteiteotretsetwetyetiatiotritwityitoatoetoitoutrotsotuotyotuatuetuitrutsutwutyu" + 041 "veeveiveovrevwevyevieviovrivwivyivoevoivrovuovyovuevuivruvwuvyu" + 042 "yaiyauyeayeeyeiyeoyiayieyioyoayoeyoiyouyuayueyuiyuo" + 043 "zaizauzvazlazwazyazeazeezeizeozvezlezwezyeziazieziozvizlizwizyizoazoezoizouzvozlozuozyozuazuezuizvuzluzwuzyu"; 044 public final Arrangement<String> items = new Arrangement<>(256, 0.5f, Hashers.caseInsensitiveStringHasher); 045 public final Arrangement<String> allAdjectives, allNouns; 046 047 /** 048 * Default constructor for a Mnemonic generator; equivalent to {@code new Mnemonic(1L)}, and probably a good choice 049 * unless you know you need different seeds. 050 * <br> 051 * This depends on the current (at the time this Mnemonic is constructed) contents of {@link Thesaurus#adjective} 052 * and {@link Thesaurus#noun}, which can be modified, and if these contents aren't identical for two different 053 * Mnemonic objects (possibly constructed at different times, using different SquidLib versions), the Mnemonics will 054 * have different encoded and decoded forms. You can either save the key set from {@link #allAdjectives} and 055 * {@link #allNouns} and pass them to {@link #Mnemonic(long, Collection, Collection)}, or save the Thesaurus state 056 * altogether using {@link Thesaurus#archiveCategories()}, loading it before constructing any Mnemonics with 057 * {@link Thesaurus#addArchivedCategories(String)}. 058 */ 059 public Mnemonic() 060 { 061 this(1L); 062 } 063 064 /** 065 * Constructor for a Mnemonic generator that allows a different seed to be chosen, which will alter the syllables 066 * produced by {@link #toMnemonic(long)} and the words produced by {@link #toWordMnemonic(int, boolean)} if you give 067 * the same numeric argument to differently-seeded Mnemonic generators. Unless you know you need this, you should 068 * probably use {@link #Mnemonic()} to ensure that your text can be decoded. 069 * <br> 070 * This depends on the current (at the time this Mnemonic is constructed) contents of {@link Thesaurus#adjective} 071 * and {@link Thesaurus#noun}, which can be modified, and if these contents aren't identical for two different 072 * Mnemonic objects (possibly constructed at different times, using different SquidLib versions), the Mnemonics will 073 * have different encoded and decoded forms. You can either save the key set from {@link #allAdjectives} and 074 * {@link #allNouns} and pass them to {@link #Mnemonic(long, Collection, Collection)}, or save the Thesaurus state 075 * altogether using {@link Thesaurus#archiveCategories()}, loading it before constructing any Mnemonics with 076 * {@link Thesaurus#addArchivedCategories(String)}. 077 * @param seed a long seed that will be used to randomize the syllables and words used. 078 */ 079 public Mnemonic(long seed) 080 { 081 RNG rng = new RNG(new LightRNG(seed)); 082 int[] order = rng.randomOrdering(431); 083 int o; 084 for (int i = 0; i < 256; i++) { 085 o = order[i]; 086 items.add(baseTriplets.substring(o * 3, o * 3 + 3)); 087 } 088 allAdjectives = new Arrangement<>(adjective.size(), 0.5f, Hashers.caseInsensitiveStringHasher); 089 allNouns = new Arrangement<>(noun.size(), 0.5f, Hashers.caseInsensitiveStringHasher); 090 for (int i = 0; i < adjective.size(); i++) { 091 ArrayList<String> words = adjective.getAt(i); 092 for (int j = 0; j < words.size(); j++) { 093 if(words.get(j).contains(" ")) 094 continue; 095 allAdjectives.add(words.get(j)); 096 } 097 } 098 allAdjectives.shuffle(rng); 099 for (int i = 0; i < noun.size(); i++) { 100 ArrayList<String> words = noun.getAt(i); 101 for (int j = 0; j < words.size(); j++) { 102 if(words.get(j).contains(" ")) 103 continue; 104 allNouns.add(words.get(j)); 105 } 106 } 107 allNouns.shuffle(rng); 108 } 109 110 /** 111 * Constructor that allows you to specify the adjective and noun collections used by 112 * {@link #toWordMnemonic(int, boolean)} as well as a seed. This should be useful when you want to enforce a stable 113 * relationship between word mnemonics produced by {@link #toWordMnemonic(int, boolean)} and the int values they 114 * decode to with {@link #fromWordMnemonic(String)}, because the default can change if the adjective and noun 115 * collections in {@link Thesaurus} change. There should be a fairly large amount of unique adjectives and nouns; 116 * {@code (long)adjectives.size() * nouns.size() * adjectives.size() * nouns.size()} should be at least 0x80000000L 117 * (2147483648L), with case disregarded. If the total is less than that, not all possible ints can be encoded with 118 * {@link #toWordMnemonic(int, boolean)}. Having 216 adjectives and 216 nouns is enough for a rough target. Each 119 * word (adjectives and nouns alike) can have any characters in it except for space, since space is used during 120 * decoding to separate words. 121 * @param seed a long seed that will be used to randomize the syllables and words used. 122 * @param adjectives a Collection of unique Strings (case-insensitive) that will be used as adjectives 123 * @param nouns a Collection of unique Strings (case-insensitive) that will be used as nouns 124 */ 125 public Mnemonic(long seed, Collection<String> adjectives, Collection<String> nouns) 126 { 127 RNG rng = new RNG(new LightRNG(seed)); 128 int[] order = rng.randomOrdering(431); 129 int o; 130 for (int i = 0; i < 256; i++) { 131 o = order[i]; 132 items.add(baseTriplets.substring(o * 3, o * 3 + 3)); 133 } 134 allAdjectives = new Arrangement<>(adjectives.size(), 0.5f, Hashers.caseInsensitiveStringHasher); 135 allNouns = new Arrangement<>(nouns.size(), 0.5f, Hashers.caseInsensitiveStringHasher); 136 for(String word : adjectives) 137 if(!word.contains(" ")) allAdjectives.add(word); 138 allAdjectives.shuffle(rng); 139 for(String word : nouns) 140 if(!word.contains(" ")) allNouns.add(word); 141 allNouns.shuffle(rng); 142 } 143 /** 144 * Constructor that allows you to specify the adjective and noun collections (given as arrays) used by 145 * {@link #toWordMnemonic(int, boolean)} as well as a seed. This should be useful when you want to enforce a stable 146 * relationship between word mnemonics produced by {@link #toWordMnemonic(int, boolean)} and the int values they 147 * decode to with {@link #fromWordMnemonic(String)}, because the default can change if the adjective and noun 148 * collections in {@link Thesaurus} change. There should be a fairly large amount of unique adjectives and nouns; 149 * {@code (long)adjectives.length * nouns.length * adjectives.length * nouns.length} should be at least 0x80000000L 150 * (2147483648L), with case disregarded. If the total is less than that, not all possible ints can be encoded with 151 * {@link #toWordMnemonic(int, boolean)}. Having 216 adjectives and 216 nouns is enough for a rough target. Each 152 * word (adjectives and nouns alike) can have any characters in it except for space, since space is used during 153 * decoding to separate words. You may want to use {@link StringKit#split(String, String)} with space or newline as 154 * the delimiter to get a String array from data containing space-separated words or data with one word per line. 155 * It's also possible to use {@link String#split(String)}, which can use {@code "\\s"} to split on any whitespace. 156 * @param seed a long seed that will be used to randomize the syllables and words used. 157 * @param adjectives an array of unique Strings (case-insensitive) that will be used as adjectives 158 * @param nouns an array of unique Strings (case-insensitive) that will be used as nouns 159 */ 160 public Mnemonic(long seed, String[] adjectives, String[] nouns) 161 { 162 RNG rng = new RNG(new LightRNG(seed)); 163 int[] order = rng.randomOrdering(431); 164 int o; 165 for (int i = 0; i < 256; i++) { 166 o = order[i]; 167 items.add(baseTriplets.substring(o * 3, o * 3 + 3)); 168 } 169 allAdjectives = new Arrangement<>(adjectives.length, 0.5f, Hashers.caseInsensitiveStringHasher); 170 allNouns = new Arrangement<>(nouns.length, 0.5f, Hashers.caseInsensitiveStringHasher); 171 for(String word : adjectives) 172 if(!word.contains(" ")) allAdjectives.add(word); 173 allAdjectives.shuffle(rng); 174 for(String word : nouns) 175 if(!word.contains(" ")) allNouns.add(word); 176 allNouns.shuffle(rng); 177 } 178 179 /** 180 * Given any long, generates a slightly-more-memorable gibberish phrase that can be decoded back to the original 181 * long with {@link #fromMnemonic(String)}. Examples of what this can produce are "noahritwimoesaidrubiotso" and 182 * "loanuiskohaimrunoizlupwi", generated by a Mnemonic with a seed of 1 from -3743983437744699304L and 183 * -8967299915041170097L, respectively. The Strings this returns are always 24 chars long, and contain only the 184 * letters a-z. 185 * @param number any long 186 * @return a 24-character String made of gibberish syllables 187 */ 188 public String toMnemonic(long number) 189 { 190 return toMnemonic(number, false); 191 } 192 193 /** 194 * Given any long, generates a slightly-more-memorable gibberish phrase that can be decoded back to the original 195 * long with {@link #fromMnemonic(String)}. Examples of what this can produce are "noahritwimoesaidrubiotso" and 196 * "loanuiskohaimrunoizlupwi", generated by a Mnemonic with a seed of 1 from -3743983437744699304L and 197 * -8967299915041170097L, respectively. The Strings this returns are always 24 chars long. If capitalize is true, 198 * then the first letter will be a capital letter from A-Z, all other letters will be a-z (including the first if 199 * capitalize is false). 200 * @param number any long 201 * @param capitalize if true, the initial letter of the returned mnemonic String will be capitalized 202 * @return a 24-character String made of gibberish syllables 203 */ 204 public String toMnemonic(long number, boolean capitalize) 205 { 206 char[] c = new char[24]; 207 String item; 208 int idx = 0; 209 item = items.keyAt((int)(number & 0xFF)); 210 c[idx++] = capitalize ? Character.toUpperCase(item.charAt(0)) : item.charAt(0); 211 c[idx++] = item.charAt(1); 212 c[idx++] = item.charAt(2); 213 214 for (int i = 8; i < 64; i+=8) { 215 item = items.keyAt((int)(number >>> i & 0xFF)); 216 c[idx++] = item.charAt(0); 217 c[idx++] = item.charAt(1); 218 c[idx++] = item.charAt(2); 219 } 220 return String.valueOf(c); 221 } 222 223 /** 224 * Takes a String produced by {@link #toMnemonic(long)} or {@link #toMnemonic(long, boolean)} and returns the long 225 * used to encode that gibberish String. This can't take just any String; if the given parameter isn't at least 24 226 * characters long, this can throw an {@link IndexOutOfBoundsException}, and if it isn't made purely from the 3-char 227 * syllables toMnemonic() produces, it won't produce a meaningful result. 228 * @param mnemonic a gibberish String produced by {@link #toMnemonic(long)} or {@link #toMnemonic(long, boolean)} 229 * @return the long used to generate {@code mnemonic} originally 230 */ 231 public long fromMnemonic(String mnemonic) 232 { 233 long result = 0L; 234 for (int i = 0; i < 8; i++) { 235 result |= (items.getInt(mnemonic.substring(i * 3, i * 3 + 3)) & 0xFFL) << (i << 3); 236 } 237 return result; 238 } 239 240 /** 241 * Given any int, generates a short phrase that can be decoded back to the original int with 242 * {@link #fromWordMnemonic(String)}. Examples of what this can produce are "Mindful warriors and the pure torch" 243 * and "Dynastic earldom and the thousandfold bandit", generated by a Mnemonic with a seed of 1 from -587415991 and 244 * -1105099633, respectively. Those Strings were generated using the current state of {@link Thesaurus} and the 245 * adjectives and nouns it stores now, and if Thesaurus is added to over time, those Strings won't correspond to 246 * those ints any more. The Strings this returns vary in length. The words this uses by default use only the letters 247 * a-z and the single quote (with A-Z for the first character if capitalize is true), with space separating words. 248 * If you constructed this Mnemonic with adjective and noun collections or arrays, then this will use only those 249 * words and will still separate words with space (and it will capitalize the first char if capitalize is true). 250 * @param number any int 251 * @param capitalize if true, the initial letter of the returned mnemonic String will be capitalized 252 * @return a short phrase that will be uniquely related to number 253 */ 254 public String toWordMnemonic(int number, boolean capitalize) 255 { 256 final int adjectiveCount = allAdjectives.size(), nounCount = allNouns.size(); 257 StringBuilder sb = new StringBuilder(80); 258 //0x000D76CB inverted by 0x000FBEE3 259 //0x000D724B inverted by 0x000E4763 260 number = (number ^ 0x7F4A7C15) * 0x000FBEE3; 261 // http://marc-b-reynolds.github.io/math/2017/10/13/XorRotate.html 262 number = ((number << 5 | number >>> 27) ^ (number << 10 | number >>> 22) ^ (number << 26 | number >>> 6)) * 0x000D724B ^ 0x91E10DA5; 263 number ^= number >>> 15; 264 boolean negative = (number < 0); 265 if(negative) number = ~number; 266 sb.append(allAdjectives.keyAt(number % adjectiveCount)).append(' ') 267 .append(allNouns.keyAt((number /= adjectiveCount) % nounCount)) 268 .append(negative ? " and the " : " of the ") 269 .append(allAdjectives.keyAt((number /= nounCount) % adjectiveCount)).append(' ') 270 .append(allNouns.keyAt((number / adjectiveCount) % nounCount)); 271 if(capitalize) 272 sb.setCharAt(0, Character.toUpperCase(sb.charAt(0))); 273 return sb.toString(); 274 } 275 276 /** 277 * Takes a String phrase produced by {@link #toWordMnemonic(int, boolean)} and returns the int used to encode that 278 * String. This can't take just any String; it must be produced by {@link #toWordMnemonic(int, boolean)} to give a 279 * meaningful result. 280 * @param mnemonic a String phrase produced by {@link #toWordMnemonic(int, boolean)} 281 * @return the int used to generate {@code mnemonic} originally 282 */ 283 public int fromWordMnemonic(String mnemonic) 284 { 285 final int adjectiveCount = allAdjectives.size(), nounCount = allNouns.size(); 286 int idx = mnemonic.indexOf(' '), factor = adjectiveCount; 287 boolean negative; 288 int result = allAdjectives.getInt(StringKit.safeSubstring(mnemonic, 0, idx)); 289 result += factor * allNouns.getInt(StringKit.safeSubstring(mnemonic, idx + 1, idx = mnemonic.indexOf(' ', idx + 1))); 290 negative = (mnemonic.charAt(idx + 1) == 'a'); 291 if(negative) idx += 8; 292 else idx += 7; 293 result += (factor *= nounCount) * allAdjectives.getInt(StringKit.safeSubstring(mnemonic, idx + 1, idx = mnemonic.indexOf(' ', idx + 1))); 294 result += factor * adjectiveCount * allNouns.getInt(StringKit.safeSubstring(mnemonic, idx + 1, -1)); 295 if(negative) result = ~result; 296 result ^= result >>> 15 ^ result >>> 30; 297 result = (result ^ 0x91E10DA5) * 0x000E4763; 298 result ^= (result << 16 | result >>> 16) ^ (result << 27 ^ result >>> 5); 299 result = result * 0x000D76CB ^ 0x7F4A7C15; 300 return result; 301 } 302}