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}