1 /**
2  * @file
3  * @brief Functions for generating random spellbooks.
4  **/
5 
6 #include "AppHdr.h"
7 
8 #include "mpr.h"
9 #include "randbook.h"
10 
11 #include <functional>
12 
13 #include "artefact.h"
14 #include "database.h"
15 #include "english.h"
16 #include "item-name.h"
17 #include "item-status-flag-type.h"
18 #include "items.h"
19 #include "religion.h"
20 #include "spl-book.h"
21 #include "stringutil.h"
22 
23 static string _gen_randbook_name(string subject, string owner,
24                                  spschool disc1, spschool disc2);
25 static string _gen_randbook_owner(god_type god, spschool disc1,
26                                   spschool disc2,
27                                   const vector<spell_type> &spells);
28 
29 /// How many spells should be in a random theme book?
theme_book_size()30 int theme_book_size() { return random2avg(4, 3) + 2; }
31 
32 /// A discipline chooser that only ever returns the given discipline.
forced_book_theme(spschool theme)33 function<spschool()> forced_book_theme(spschool theme)
34 {
35     return [theme]() { return theme; };
36 }
37 
38 /// Choose a random valid discipline for a themed randbook.
random_book_theme()39 spschool random_book_theme()
40 {
41     vector<spschool> disciplines;
42     for (auto discipline : spschools_type::range())
43         disciplines.push_back(discipline);
44     return disciplines[random2(disciplines.size())];
45 }
46 
47 /**
48  * Attempt to choose a valid discipline for a themed randbook which contains
49  * at least the given spells.
50  *
51  * XXX: really we should be trying to create a pair that covers the set,
52  * rather than trying to do it all with one...
53  *
54  * @param forced_spells     A set of spells guaranteed to be in the book.
55  * @return                  A discipline which will match as many of those
56  *                          spells as possible.
57  */
matching_book_theme(const vector<spell_type> & forced_spells)58 spschool matching_book_theme(const vector<spell_type> &forced_spells)
59 {
60     map<spschool, int> seen_disciplines;
61     for (auto spell : forced_spells)
62     {
63         const spschools_type disciplines = get_spell_disciplines(spell);
64         for (auto discipline : spschools_type::range())
65             if (disciplines & discipline)
66                 ++seen_disciplines[discipline];
67     }
68 
69     bool matched = false;
70     for (auto seen : seen_disciplines)
71     {
72         if (seen.second == (int)forced_spells.size())
73         {
74             matched = true;
75             break;
76         }
77     }
78 
79     if (!matched)
80     {
81         const spschool *discipline
82             = random_choose_weighted(seen_disciplines);
83         if (discipline)
84             return *discipline;
85         return random_book_theme();
86     }
87 
88     for (auto seen : seen_disciplines)
89         seen.second = seen.second == (int)forced_spells.size() ? 1 : 0;
90     const spschool *discipline
91         = random_choose_weighted(seen_disciplines);
92     ASSERT(discipline);
93     return *discipline;
94 }
95 
96 /**
97  * Can we include the given spell in our spellbook?
98  *
99  * @param agent             The entity creating the book; possibly a god.
100  * @param spell             The spell to be filtered.
101  * @return                  Whether the spell can be included.
102  */
_agent_spell_filter(int agent,spell_type spell)103 static bool _agent_spell_filter(int agent, spell_type spell)
104 {
105     // Only use actual player spells.
106     if (!is_player_book_spell(spell))
107         return false;
108 
109     // Don't include spells a god dislikes, if this is an acquirement
110     // or a god gift.
111     const god_type god = agent >= AQ_SCROLL ? you.religion : (god_type)agent;
112     if (god_hates_spell(spell, god))
113         return false;
114 
115     return true;
116 }
117 
118 /**
119  * Can we include the given spell in our themed spellbook?
120  *
121  * @param discipline_1      The first spellschool of the book.
122  * @param discipline_2      The second spellschool of the book.
123  * @param agent             The entity creating the book; possibly a god.
124  * @param prev              A list of spells already chosen for the book.
125  * @param spell             The spell to be filtered.
126  * @return                  Whether the spell can be included.
127  */
basic_themed_spell_filter(spschool discipline_1,spschool discipline_2,int agent,const vector<spell_type> & prev,spell_type spell)128 bool basic_themed_spell_filter(spschool discipline_1,
129                                spschool discipline_2,
130                                int agent,
131                                const vector<spell_type> &prev,
132                                spell_type spell)
133 {
134     if (!is_valid_spell(spell))
135         return false;
136 
137     // Only include spells matching at least one of the book's disciplines.
138     const spschools_type disciplines = get_spell_disciplines(spell);
139     if (!(disciplines & discipline_1) && !(disciplines & discipline_2))
140         return false;
141 
142     // Only include spells we haven't already.
143     if (count(prev.begin(), prev.end(), spell))
144         return false;
145 
146     if (!_agent_spell_filter(agent, spell))
147         return false;
148 
149     return true;
150 }
151 
152 /**
153  * Build and return a spell filter that excludes spells that would push us over
154  * the maximum total spell levels allowed in the book.
155  *
156  * @param max_levels    The max total spell levels allowed.
157  * @param subfilter     A filter to check further.
158  */
capped_spell_filter(int max_levels,themed_spell_filter subfilter)159 themed_spell_filter capped_spell_filter(int max_levels,
160                                         themed_spell_filter subfilter)
161 {
162     if (max_levels < 1)
163         return subfilter; // don't even bother.
164 
165     return [max_levels, subfilter](spschool discipline_1,
166                                    spschool discipline_2,
167                                    int agent,
168                                    const vector<spell_type> &prev,
169                                    spell_type spell)
170     {
171         if (!subfilter(discipline_1, discipline_2, agent, prev, spell))
172             return false;
173 
174         int prev_levels = 0;
175         for (auto prev_spell : prev)
176             prev_levels += spell_difficulty(prev_spell);
177         if (spell_difficulty(spell) + prev_levels > max_levels)
178             return false;
179 
180         return true;
181     };
182 }
183 
184 /**
185  * Build and return a spell filter that forces the first several spells to
186  * be from the given list, disregarding other constraints
187  *
188  * @param forced_spells     Spells to force.
189  * @param subfilter         A filter to check after all forced spells are in.
190  */
forced_spell_filter(const vector<spell_type> & forced_spells,themed_spell_filter subfilter)191 themed_spell_filter forced_spell_filter(const vector<spell_type> &forced_spells,
192                                         themed_spell_filter subfilter)
193 {
194     return [&forced_spells, subfilter](spschool discipline_1,
195                                        spschool discipline_2,
196                                        int agent,
197                                        const vector<spell_type> &prev,
198                                        spell_type spell)
199     {
200         if (prev.size() < forced_spells.size())
201             return spell == forced_spells[prev.size()];
202         return subfilter(discipline_1, discipline_2, agent, prev, spell);
203     };
204 }
205 
206 /**
207  * Generate a list of spells for a themebook.
208  *
209  * @param discipline_1      The first spellschool of the book.
210  * @param discipline_2      The second spellschool of the book.
211  * @param filter            A filter specifying which spells can be included.
212  * @param agent             The entity creating the book; possibly a god.
213  * @param num_spells        How many spells should be included.
214  * @param spells[out]       The list to be populated.
215  */
theme_book_spells(spschool discipline_1,spschool discipline_2,themed_spell_filter filter,int agent,int num_spells,vector<spell_type> & spells)216 void theme_book_spells(spschool discipline_1,
217                        spschool discipline_2,
218                        themed_spell_filter filter,
219                        int agent,
220                        int num_spells,
221                        vector<spell_type> &spells)
222 {
223     ASSERT(num_spells >= 1);
224     for (int i = 0; i < num_spells; ++i)
225     {
226         vector<spell_type> possible_spells;
227         for (int s = 0; s < NUM_SPELLS; ++s)
228         {
229             const spell_type spell = static_cast<spell_type>(s);
230             if (filter(discipline_1, discipline_2, agent, spells, spell))
231                 possible_spells.push_back(spell);
232         }
233 
234         if (!possible_spells.size())
235         {
236             dprf("Couldn't find any valid spell for slot %d!", i);
237             return;
238         }
239 
240         spells.push_back(possible_spells[random2(possible_spells.size())]);
241     }
242 
243     ASSERT(spells.size());
244 }
245 
246 /**
247  * Try to remove any discipline that's not actually being used by a given
248  * randbook, setting it to the other (used) discipline.
249  *
250  * E.g., if a cj/ne randbook is generated with only cj spells, set discipline_2
251  * to cj as well.
252  *
253  * @param discipline_1[in,out]      The first book discipline.
254  * @param discipline_1[in,out]      The second book discipline.
255  * @param spells[in]                The list of spells the book should contain.
256  */
fixup_randbook_disciplines(spschool & discipline_1,spschool & discipline_2,const vector<spell_type> & spells)257 void fixup_randbook_disciplines(spschool &discipline_1,
258                                 spschool &discipline_2,
259                                 const vector<spell_type> &spells)
260 {
261     bool has_d1 = false, has_d2 = false;
262     for (auto spell : spells)
263     {
264         const spschools_type disciplines = get_spell_disciplines(spell);
265         if (disciplines & discipline_1)
266             has_d1 = true;
267         if (disciplines & discipline_2)
268             has_d2 = true;
269     }
270 
271     if (has_d1 == has_d2)
272         return; // both schools or neither used; can't do anything regardless
273 
274     if (has_d1)
275         discipline_2 = discipline_1;
276     else
277         discipline_1 = discipline_2;
278 }
279 
280 /**
281  * Turn a given book into a themed spellbook.
282  *
283  * @param book[in,out]      The book in question.
284  * @param filter            A filter specifying which spells can be included.
285  * @param get_discipline    A function to choose themes for the book.
286  * @param num_spells        The number of spells the book should include.
287  *                          Not guaranteed, but should be fairly reliable.
288  * @param owner             The name of the book's owner, if any. Cosmetic.
289  * @param subject           The subject of the book, if any. Cosmetic.
290  */
build_themed_book(item_def & book,themed_spell_filter filter,function<spschool ()> get_discipline,int num_spells,string owner,string subject)291 void build_themed_book(item_def &book, themed_spell_filter filter,
292                        function<spschool()> get_discipline,
293                        int num_spells, string owner, string subject)
294 {
295     if (num_spells < 1)
296         num_spells = theme_book_size();
297 
298     spschool discipline_1 = get_discipline();
299     spschool discipline_2 = get_discipline();
300 
301     item_source_type agent;
302     if (!origin_is_acquirement(book, &agent))
303         agent = (item_source_type)origin_as_god_gift(book);
304 
305     vector<spell_type> spells;
306     theme_book_spells(discipline_1, discipline_2, filter, agent, num_spells,
307                       spells);
308     fixup_randbook_disciplines(discipline_1, discipline_2, spells);
309     init_book_theme_randart(book, spells);
310     name_book_theme_randart(book, discipline_1, discipline_2, owner, subject);
311 }
312 
_compare_spells(spell_type a,spell_type b)313 static bool _compare_spells(spell_type a, spell_type b)
314 {
315     if (a == SPELL_NO_SPELL && b == SPELL_NO_SPELL)
316         return false;
317     else if (a != SPELL_NO_SPELL && b == SPELL_NO_SPELL)
318         return true;
319     else if (a == SPELL_NO_SPELL && b != SPELL_NO_SPELL)
320         return false;
321 
322     int level_a = spell_difficulty(a);
323     int level_b = spell_difficulty(b);
324 
325     if (level_a != level_b)
326         return level_a < level_b;
327 
328     spschools_type schools_a = get_spell_disciplines(a);
329     spschools_type schools_b = get_spell_disciplines(b);
330 
331     if (schools_a != schools_b && schools_a != spschool::none
332         && schools_b != spschool::none)
333     {
334         const char* a_type = nullptr;
335         const char* b_type = nullptr;
336 
337         // Find lowest/earliest school for each spell.
338         for (const auto mask : spschools_type::range())
339         {
340             if (a_type == nullptr && (schools_a & mask))
341                 a_type = spelltype_long_name(mask);
342             if (b_type == nullptr && (schools_b & mask))
343                 b_type = spelltype_long_name(mask);
344         }
345         ASSERT(a_type != nullptr);
346         ASSERT(b_type != nullptr);
347         return strcmp(a_type, b_type) < 0;
348     }
349 
350     return strcmp(spell_title(a), spell_title(b)) < 0;
351 }
352 
_get_spell_list(vector<spell_type> & spells,int level,god_type god,bool avoid_uncastable,int & god_discard,int & uncastable_discard,bool avoid_known=false)353 static void _get_spell_list(vector<spell_type> &spells, int level,
354                             god_type god, bool avoid_uncastable,
355                             int &god_discard, int &uncastable_discard,
356                             bool avoid_known = false)
357 {
358     for (int i = 0; i < NUM_SPELLS; ++i)
359     {
360         const spell_type spell = (spell_type) i;
361 
362         if (!is_valid_spell(spell))
363             continue;
364 
365         // Only use actual player spells.
366         if (!is_player_book_spell(spell))
367             continue;
368 
369         if (avoid_known && you.spell_library[spell])
370             continue;
371 
372         // fixed level randart: only include spells of the given level
373         if (level != -1 && spell_difficulty(spell) != level)
374             continue;
375 
376         if (avoid_uncastable && !you_can_memorise(spell))
377         {
378             uncastable_discard++;
379             continue;
380         }
381 
382         if (god_hates_spell(spell, god))
383         {
384             god_discard++;
385             continue;
386         }
387 
388         // Passed all tests.
389         spells.push_back(spell);
390     }
391 }
392 
_make_book_randart(item_def & book)393 static void _make_book_randart(item_def &book)
394 {
395     if (!is_artefact(book))
396     {
397         book.flags |= ISFLAG_RANDART;
398         if (!book.props.exists(ARTEFACT_APPEAR_KEY))
399         {
400             book.props[ARTEFACT_APPEAR_KEY].get_string() =
401             make_artefact_name(book, true);
402         }
403     }
404 }
405 
406 /**
407  * Choose an owner for a randomly-generated single-level spellbook.
408  *
409  * @param god       The god responsible for the book, if any.
410  *                  If set, will be the book's owner.
411  * @return          An owner for the book; may be the empty string.
412  */
_gen_randlevel_owner(god_type god)413 static string _gen_randlevel_owner(god_type god)
414 {
415     if (god != GOD_NO_GOD)
416         return god_name(god, false);
417     if (one_chance_in(30))
418         return god_name(GOD_SIF_MUNA, false);
419     if (one_chance_in(3))
420         return make_name();
421     return "";
422 }
423 
424 /// What's the DB lookup string for a given randbook spell level?
_randlevel_difficulty_name(int level)425 static string _randlevel_difficulty_name(int level)
426 {
427     if (level == 1)
428         return "starting";
429     if (level <= 3 || level == 4 && coinflip())
430         return "easy";
431     if (level <= 6)
432         return "moderate";
433     return "difficult";
434 }
435 
436 /**
437  * Generate a name for a randomly-generated single-level spellbook.
438  *
439  * @param level     The level of the spells in the book.
440  * @param god       The god responsible for the book, if any.
441  * @return          A spellbook name. May contain placeholders (@foo@).
442  */
_gen_randlevel_name(int level,god_type god)443 static string _gen_randlevel_name(int level, god_type god)
444 {
445     const string owner_name = _gen_randlevel_owner(god);
446     const bool has_owner = !owner_name.empty();
447     const string apostrophised_owner = owner_name.empty() ? "" :
448     apostrophise(owner_name) + " ";
449 
450     if (god == GOD_XOM && coinflip())
451     {
452         const string xomname = getRandNameString("book_noun") + " of "
453         + getRandNameString("Xom_book_title");
454         return apostrophised_owner + xomname;
455     }
456 
457     const string lookup = _randlevel_difficulty_name(level) + " level book";
458 
459     // First try for names respecting the book's previous owner/author
460     // (if one exists), then check for general difficulty.
461     string bookname;
462     if (has_owner)
463         bookname = getRandNameString(lookup + " owner");
464 
465     if (bookname.empty())
466         bookname = getRandNameString(lookup);
467 
468     bookname = uppercase_first(bookname);
469     if (has_owner)
470     {
471         if (bookname.substr(0, 4) == "The ")
472             bookname = bookname.substr(4);
473         else if (bookname.substr(0, 2) == "A ")
474             bookname = bookname.substr(2);
475         else if (bookname.substr(0, 3) == "An ")
476             bookname = bookname.substr(3);
477     }
478 
479     if (bookname.find("@level@", 0) != string::npos)
480     {
481         const string level_name = uppercase_first(number_in_words(level));
482         bookname = replace_all(bookname, "@level@", level_name);
483     }
484 
485     if (bookname.empty())
486         bookname = getRandNameString("book");
487 
488     return apostrophised_owner + bookname;
489 }
490 
_set_book_spell_list(item_def & book,vector<spell_type> spells)491 void _set_book_spell_list(item_def &book, vector<spell_type> spells)
492 {
493     ASSERT(!spells.empty());
494     sort(begin(spells), end(spells), _compare_spells);
495     spells.resize(RANDBOOK_SIZE, SPELL_NO_SPELL);
496 
497     CrawlHashTable &props = book.props;
498     props.erase(SPELL_LIST_KEY);
499     props[SPELL_LIST_KEY].new_vector(SV_INT).resize(RANDBOOK_SIZE);
500 
501     CrawlVector &spell_vec = props[SPELL_LIST_KEY].get_vector();
502     spell_vec.set_max_size(RANDBOOK_SIZE);
503 
504     for (int i = 0; i < RANDBOOK_SIZE; i++)
505         spell_vec[i].get_int() = spells[i];
506 }
507 
508 /**
509  * Turn the given book into a randomly-generated spellbook ("randbook"),
510  * containing only spells of a given level.
511  *
512  * @param book[out]    The book in question.
513  * @param level        The level of the spells. If -1, choose a level randomly.
514  * @param god            Is this a gift from Sif Muna?
515  * @return             Whether the book was successfully transformed.
516  */
make_book_level_randart(item_def & book,int level,bool sif)517 bool make_book_level_randart(item_def &book, int level, bool sif)
518 {
519     ASSERT(book.base_type == OBJ_BOOKS);
520 
521     const god_type god = origin_as_god_gift(book);
522 
523     const bool completely_random =
524         god == GOD_XOM || (god == GOD_NO_GOD && !origin_is_acquirement(book));
525 
526     if (level == -1)
527     {
528         int max_level =
529             (completely_random ? 9
530              : min(9, you.get_experience_level()));
531 
532         level = random_range(1, max_level);
533     }
534     ASSERT_RANGE(level, 0 + 1, 9 + 1);
535     // Book level:       1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
536     // Number of spells: 5 | 5 | 5 | 6 | 6 | 6 | 4 | 2 | 1
537     int num_spells = max(1, min(5 + (level - 1)/3,
538                                 18 - 2*level));
539     // Sif Muna retains the old randbook sizes.
540     // Other level randbooks shrink to modern book sizes.
541     if (!sif)
542         num_spells = max(1, div_rand_round(num_spells * 3, 5));
543     ASSERT_RANGE(num_spells, 0 + 1, RANDBOOK_SIZE + 1);
544 
545     book.sub_type = BOOK_RANDART_LEVEL;
546     _make_book_randart(book);
547 
548     int god_discard        = 0;
549     int uncastable_discard = 0;
550 
551     vector<spell_type> spells;
552     // Which spells are valid choices?
553     _get_spell_list(spells, level, god, !completely_random,
554                     god_discard, uncastable_discard);
555 
556     if (spells.empty())
557     {
558         if (level > 1)
559             return make_book_level_randart(book, level - 1);
560         char buf[80];
561 
562         if (god_discard > 0 && uncastable_discard == 0)
563         {
564             snprintf(buf, sizeof(buf), "%s disliked all level %d spells",
565                      god_name(god).c_str(), level);
566         }
567         else if (god_discard == 0 && uncastable_discard > 0)
568             sprintf(buf, "No level %d spells can be cast by you", level);
569         else if (god_discard > 0 && uncastable_discard > 0)
570         {
571             snprintf(buf, sizeof(buf),
572                      "All level %d spells are either disliked by %s "
573                      "or cannot be cast by you.",
574                      level, god_name(god).c_str());
575         }
576         else
577             sprintf(buf, "No level %d spells?!?!?!", level);
578 
579         mprf(MSGCH_ERROR, "Could not create fixed level randart spellbook: %s",
580              buf);
581 
582         return false;
583     }
584     shuffle_array(spells);
585 
586     if (num_spells > (int) spells.size())
587     {
588         num_spells = spells.size();
589 #if defined(DEBUG) || defined(DEBUG_DIAGNOSTICS)
590         mprf(MSGCH_WARN, "More spells requested for fixed level (%d) "
591              "randart spellbook than there are valid spells.",
592              level);
593         mprf(MSGCH_WARN, "Discarded %d spells due to being uncastable and "
594              "%d spells due to being disliked by %s.",
595              uncastable_discard, god_discard, god_name(god).c_str());
596 #endif
597     }
598 
599     vector<bool> spell_used(spells.size(), false);
600     vector<bool> avoid_memorised(spells.size(), !completely_random);
601     vector<bool> avoid_seen(spells.size(), !completely_random);
602 
603     vector<spell_type> chosen_spells(RANDBOOK_SIZE, SPELL_NO_SPELL);
604 
605     int book_pos = 0;
606     while (book_pos < num_spells)
607     {
608         int spell_pos = random2(spells.size());
609 
610         if (spell_used[spell_pos])
611             continue;
612 
613         spell_type spell = spells[spell_pos];
614         ASSERT(spell != SPELL_NO_SPELL);
615 
616         if (avoid_memorised[spell_pos] && you.has_spell(spell))
617         {
618             // Only once.
619             avoid_memorised[spell_pos] = false;
620             continue;
621         }
622 
623         if (avoid_seen[spell_pos] && you.spell_library[spell] && coinflip())
624         {
625             // Only once.
626             avoid_seen[spell_pos] = false;
627             continue;
628         }
629 
630         spell_used[spell_pos]     = true;
631         chosen_spells.push_back(spell);
632         book_pos++;
633     }
634 
635     _set_book_spell_list(book, chosen_spells);
636 
637     const string name = _gen_randlevel_name(level, god);
638     set_artefact_name(book, replace_name_parts(name, book));
639     // None of these books need a definite article prepended.
640     book.props[BOOK_TITLED_KEY].get_bool() = true;
641 
642     return true;
643 }
644 
645 /**
646  * Initialize a themed randbook, & fill it with the given spells.
647  *
648  * @param book[in,out]      The book to be initialized.
649  * @param spells            The spells to fill the book with.
650  *                          Not passed by reference since we want to sort it.
651  */
init_book_theme_randart(item_def & book,vector<spell_type> spells)652 void init_book_theme_randart(item_def &book, vector<spell_type> spells)
653 {
654     book.sub_type = BOOK_RANDART_THEME;
655     _make_book_randart(book);
656     _set_book_spell_list(book, move(spells));
657 }
658 
659 /**
660  * Generate and apply a name for a themed randbook.
661  *
662  * @param book[in,out]      The book to be named.
663  * @param discipline_1      The first spellschool.
664  * @param discipline_2      The second spellschool.
665  * @param owner             The book's owner; e.g. "Xom". May be empty.
666  * @param subject           The subject of the book. May be empty.
667  */
name_book_theme_randart(item_def & book,spschool discipline_1,spschool discipline_2,string owner,string subject)668 void name_book_theme_randart(item_def &book, spschool discipline_1,
669                              spschool discipline_2,
670                              string owner, string subject)
671 {
672     if (owner.empty())
673     {
674         const vector<spell_type> spells = spells_in_book(book);
675         owner = _gen_randbook_owner(origin_as_god_gift(book), discipline_1,
676                                     discipline_2, spells);
677     }
678 
679     book.props[BOOK_TITLED_KEY].get_bool() = !owner.empty();
680     const string name = _gen_randbook_name(subject, owner,
681                                            discipline_1, discipline_2);
682     set_artefact_name(book, replace_name_parts(name, book));
683 }
684 
685 /**
686  * Possibly generate a 'subject' for a book based on its owner.
687  *
688  * @param owner     The book's owner; e.g. "Xom".
689  * @return          A random book subject, or the empty string.
690  *                  May contain placeholders (@foo@).
691  */
_maybe_gen_book_subject(string owner)692 static string _maybe_gen_book_subject(string owner)
693 {
694     // Sometimes use a completely random title.
695     if (owner == "Xom" && !one_chance_in(20))
696         return getRandNameString("Xom_book_title");
697     if (one_chance_in(20) && (owner.empty() || one_chance_in(3)))
698         return getRandNameString("random_book_title");
699     return "";
700 }
701 
702 /**
703  * Generates a random, vaguely appropriate name for a randbook.
704  *
705  * @param   subject     The subject of the book. If non-empty, the book will
706  *                      have a name of the form "[Foo] of <subject>".
707  * @param   owner       The name of the book's 'owner', if any.
708  *                      (E.g., Xom, Cerebov, Boris...)
709  *                      Prepended to the book's name (Foo's...); "Xom" has
710  *                      further effects.
711  * @param   disc1       A spellschool (discipline) associated with the book.
712  * @param   disc2       A spellschool (discipline) associated with the book.
713  * @return              A book name. May contain placeholders (@foo@).
714  */
_gen_randbook_name(string subject,string owner,spschool disc1,spschool disc2)715 static string _gen_randbook_name(string subject, string owner,
716                                  spschool disc1,
717                                  spschool disc2)
718 {
719     const string apostrophised_owner = owner.empty() ?
720         "" :
721         apostrophise(owner) + " ";
722 
723     const string real_subject = subject.empty() ?
724         _maybe_gen_book_subject(owner) :
725         subject;
726 
727     if (!real_subject.empty())
728     {
729         return make_stringf("%s%s of %s",
730                             apostrophised_owner.c_str(),
731                             getRandNameString("book_noun").c_str(),
732                             real_subject.c_str());
733     }
734 
735     string name = apostrophised_owner;
736 
737     // Give a name that reflects the primary and secondary
738     // spell disciplines of the spells contained in the book.
739     name += getRandNameString("book_name") + " ";
740 
741     // For the actual name there's a 66% chance of getting something like
742     //  <book> of the Fiery Traveller (Translocation/Fire), else
743     //  <book> of Displacement and Flames.
744     string type_name;
745     if (disc1 != disc2 && !one_chance_in(3))
746     {
747         string lookup = spelltype_long_name(disc2);
748         type_name = getRandNameString(lookup + " adj");
749     }
750 
751     if (type_name.empty())
752     {
753         // No adjective found, use the normal method of combining two nouns.
754         type_name = getRandNameString(spelltype_long_name(disc1));
755         if (type_name.empty())
756             name += spelltype_long_name(disc1);
757         else
758             name += type_name;
759 
760         if (disc1 != disc2)
761         {
762             name += " and ";
763             type_name = getRandNameString(spelltype_long_name(disc2));
764 
765             if (type_name.empty())
766                 name += spelltype_long_name(disc2);
767             else
768                 name += type_name;
769         }
770     }
771     else
772     {
773         string bookname = type_name + " ";
774 
775         // Add the noun for the first discipline.
776         type_name = getRandNameString(spelltype_long_name(disc1));
777         if (type_name.empty())
778             bookname += spelltype_long_name(disc1);
779         else
780         {
781             if (type_name.find("the ", 0) != string::npos)
782             {
783                 type_name = replace_all(type_name, "the ", "");
784                 bookname = "the " + bookname;
785             }
786             bookname += type_name;
787         }
788         name += bookname;
789     }
790 
791     return name;
792 }
793 
794 /**
795  * Possibly choose a random 'owner' for a themed random spellbook.
796  *
797  * @param god           The god responsible for gifting the book, if any.
798  * @param disc1         A spellschool (discipline) associated with the book.
799  * @param disc2         A spellschool (discipline) associated with the book.
800  * @param spells        The spells in the book.
801  * @return              The name of the book's 'owner', or the empty string.
802  */
_gen_randbook_owner(god_type god,spschool disc1,spschool disc2,const vector<spell_type> & spells)803 static string _gen_randbook_owner(god_type god, spschool disc1,
804                                   spschool disc2,
805                                   const vector<spell_type> &spells)
806 {
807     // If the owner hasn't been set already use
808     // a) the god's name for god gifts (only applies to Sif Muna and Xom),
809     // b) a name depending on the spell disciplines, for pure books
810     // c) a random name (all god gifts not named earlier)
811     // d) an applicable god's name
812     // ... else leave it unnamed (around 57% chance for non-god gifts)
813 
814     int highest_level = 0;
815     int lowest_level = 27;
816     bool all_spells_disc1 = true;
817     for (auto spell : spells)
818     {
819         const int level = spell_difficulty(spell);
820         highest_level = max(level, highest_level);
821         lowest_level = min(level, lowest_level);
822 
823         if (!(get_spell_disciplines(spell) & disc1))
824             all_spells_disc1 = false;
825     }
826 
827     // this logic is very odd...
828     const bool highlevel = highest_level >= 7 + random2(3)
829                            && (lowest_level > 1 || coinflip());
830 
831 
832     // name of gifting god?
833     const bool god_gift = god != GOD_NO_GOD;
834     if (god_gift && !one_chance_in(4))
835         return god_name(god, false);
836 
837     // thematically appropriate name?
838     if (god_gift && one_chance_in(3) || one_chance_in(5))
839     {
840         vector<string> lookups;
841         const string d1_name = spelltype_long_name(disc1);
842 
843         if (disc1 != disc2)
844         {
845             const string lookup = d1_name + " " + spelltype_long_name(disc2);
846             if (highlevel)
847                 lookups.push_back("highlevel " + lookup + " owner");
848             lookups.push_back(lookup + " owner");
849         }
850 
851         if (all_spells_disc1)
852         {
853             if (highlevel)
854                 lookups.push_back("highlevel " + d1_name + " owner");
855             lookups.push_back(d1_name + "owner");
856         }
857 
858         for (string &lookup : lookups)
859         {
860             const string owner = getRandNameString(lookup);
861             if (!owner.empty() && owner != "__NONE")
862                 return owner;
863         }
864     }
865 
866     // random name?
867     if (god_gift || one_chance_in(5))
868         return make_name();
869 
870     // applicable god's name?
871     if (!god_gift && one_chance_in(9))
872     {
873         switch (disc1)
874         {
875             case spschool::necromancy:
876                 if (all_spells_disc1 && !one_chance_in(6))
877                     return god_name(GOD_KIKUBAAQUDGHA, false);
878                 break;
879             case spschool::conjuration:
880                 if (all_spells_disc1 && !one_chance_in(4))
881                     return god_name(GOD_VEHUMET, false);
882                 break;
883             default:
884                 break;
885         }
886         return god_name(GOD_SIF_MUNA, false);
887     }
888 
889     return "";
890 }
891 
892 // Give Roxanne a randart spellbook of the disciplines Transmutations/Earth
893 // that includes Statue Form and is named after her.
make_book_roxanne_special(item_def * book)894 void make_book_roxanne_special(item_def *book)
895 {
896     spschool disc = random_choose(spschool::transmutation, spschool::earth);
897     vector<spell_type> forced_spell = {SPELL_STATUE_FORM};
898     build_themed_book(*book,
899                       forced_spell_filter(forced_spell,
900                                            capped_spell_filter(19)),
901                       forced_book_theme(disc), 5, "Roxanne");
902 }
903 
904 /// Does the given acq source generate books totally randomly?
_completely_random_books(int agent)905 static bool _completely_random_books(int agent)
906 {
907     // only acq & god gifts from sane gods weight spells/disciplines
908     // for player utility.
909     return agent == GOD_XOM || agent == GOD_NO_GOD;
910 }
911 
912 /// How desireable is the given spell for inclusion in an acquired randbook?
_randbook_spell_weight(spell_type spell,int agent)913 static int _randbook_spell_weight(spell_type spell, int agent)
914 {
915     if (_completely_random_books(agent))
916         return 1;
917 
918     // prefer unseen spells
919     const int seen_weight = you.spell_library[spell] ? 1 : 4;
920 
921     // prefer spells roughly approximating the player's overall spellcasting
922     // ability (?????)
923     const int Spc = div_rand_round(you.skill(SK_SPELLCASTING, 256, true), 256);
924     const int difficult_weight = 5 - abs(3 * spell_difficulty(spell) - Spc) / 7;
925 
926     // prefer spells in disciplines the player is skilled with
927     const spschools_type disciplines = get_spell_disciplines(spell);
928     int total_skill = 0;
929     int num_skills  = 0;
930     for (const auto disc : spschools_type::range())
931     {
932         if (disciplines & disc)
933         {
934             const skill_type sk = spell_type2skill(disc);
935             total_skill += div_rand_round(you.skill(sk, 256, true), 256);
936             num_skills++;
937         }
938     }
939     int skill_weight = 1;
940     if (num_skills > 0)
941         skill_weight = (2 + (total_skill / num_skills)) / 3;
942     skill_weight = max(1, skill_weight);
943 
944     const int weight = seen_weight * skill_weight * difficult_weight;
945     ASSERT(weight > 0);
946     return weight;
947     /// XXX: I'm not sure how much impact all this actually has.
948 }
949 
950 typedef map<spell_type, int> weighted_spells;
951 
952 /**
953  * Populate a list of possible spells to be included in acquired books,
954  * weighted by desireability.
955  *
956  * @param possible_spells[out]  The list to be populated.
957  * @param agent         The entity creating the item; possibly a god.
958  */
_get_weighted_randbook_spells(weighted_spells & possible_spells,int agent)959 static void _get_weighted_randbook_spells(weighted_spells &possible_spells,
960                                           int agent)
961 {
962     for (int i = 0; i < NUM_SPELLS; ++i)
963     {
964         const spell_type spell = (spell_type) i;
965 
966         if (!is_valid_spell(spell)
967             || !_agent_spell_filter(agent, spell)
968             || !you_can_memorise(spell))
969         {
970             continue;
971         }
972 
973         // Passed all tests.
974         const int weight = _randbook_spell_weight(spell, agent);
975         possible_spells[spell] = weight;
976     }
977 }
978 
979 /**
980  * Choose a spell discipline for a randbook, weighted by the the value of all
981  * possible spells in that discipline.
982  *
983  * @param possibles     A weighted list of all possible spells to include in
984  *                      the book.
985  * @param agent         The entity creating the item; possibly a god.
986  * @return              An appropriate spell school; e.g. spschool::fire.
987  */
_choose_randbook_discipline(weighted_spells & possible_spells,int agent)988 static spschool _choose_randbook_discipline(weighted_spells
989                                                       &possible_spells,
990                                                       int agent)
991 {
992     map<spschool, int> discipline_weights;
993     for (auto weighted_spell : possible_spells)
994     {
995         const spell_type spell = weighted_spell.first;
996         const int weight = weighted_spell.second;
997         const spschools_type disciplines = get_spell_disciplines(spell);
998         for (const auto disc : spschools_type::range())
999         {
1000             if (disciplines & disc)
1001             {
1002                 if (_completely_random_books(agent))
1003                     discipline_weights[disc] = 1;
1004                 else
1005                     discipline_weights[disc] += weight;
1006             }
1007         }
1008     }
1009 
1010     const spschool *discipline
1011         = random_choose_weighted(discipline_weights);
1012     ASSERT(discipline);
1013     return *discipline;
1014 }
1015 
1016 /**
1017  * From a given weighted list of possible spells, choose a set to include in
1018  * a randbook, filtered by discipline.
1019  *
1020  * @param[in,out] possible_spells   All possible spells, weighted by value.
1021  Modified in-place for efficiency.
1022  * @param discipline_1              The first spellschool.
1023  * @param discipline_2              The second spellschool.
1024  * @param size                      The number of spells to include.
1025  * @param[out] spells               The chosen spells.
1026  */
_choose_themed_randbook_spells(weighted_spells & possible_spells,spschool discipline_1,spschool discipline_2,int size,vector<spell_type> & spells)1027 static void _choose_themed_randbook_spells(weighted_spells &possible_spells,
1028                                            spschool discipline_1,
1029                                            spschool discipline_2,
1030                                            int size, vector<spell_type> &spells)
1031 {
1032     for (auto &weighted_spell : possible_spells)
1033     {
1034         const spell_type spell = weighted_spell.first;
1035         const spschools_type disciplines = get_spell_disciplines(spell);
1036         if (!(disciplines & discipline_1) && !(disciplines & discipline_2))
1037             weighted_spell.second = 0; // filter it out
1038     }
1039 
1040     for (int i = 0; i < size; ++i)
1041     {
1042         const spell_type *spell = random_choose_weighted(possible_spells);
1043         if (!spell)
1044             break;
1045         spells.push_back(*spell);
1046         possible_spells[*spell] = 0; // don't choose the same one twice!
1047     }
1048     // `size` is guaranteed to be >0 by an ASSERT in the calling function
1049     ASSERT(spells.size() > 0);
1050 }
1051 
1052 /**
1053  * Turn a given book into an acquirement-quality themed spellbook.
1054  *
1055  * @param book[out]     The book to be turned into a randbook.
1056  * @param agent         The entity creating the item; possibly a god.
1057  */
acquire_themed_randbook(item_def & book,int agent)1058 void acquire_themed_randbook(item_def &book, int agent)
1059 {
1060     weighted_spells possible_spells;
1061     _get_weighted_randbook_spells(possible_spells, agent);
1062 
1063     // include 2-8 spells in the book, leaning heavily toward 5
1064     const int size = min(2 + random2avg(7, 3),
1065                          (int)possible_spells.size());
1066     ASSERT(size);
1067 
1068     // XXX: we could cache this...
1069     spschool discipline_1
1070         = _choose_randbook_discipline(possible_spells, agent);
1071     spschool discipline_2
1072         = _choose_randbook_discipline(possible_spells, agent);
1073 
1074     vector<spell_type> spells;
1075     _choose_themed_randbook_spells(possible_spells, discipline_1, discipline_2,
1076                                    size, spells);
1077 
1078     fixup_randbook_disciplines(discipline_1, discipline_2, spells);
1079 
1080     // Acquired randart books have a chance of being named after the player.
1081     const string owner = agent == AQ_SCROLL && one_chance_in(12) ?
1082         you.your_name :
1083         "";
1084 
1085     init_book_theme_randart(book, spells);
1086     name_book_theme_randart(book, discipline_1, discipline_2, owner);
1087 }
1088