1 /**
2  * @file
3  * @brief Functions related to the monster arena (stage and watch fights).
4 **/
5 
6 #include "AppHdr.h"
7 
8 #include "arena.h"
9 
10 #include <stdexcept>
11 
12 #include "act-iter.h"
13 #include "colour.h"
14 #include "command.h"
15 #include "dungeon.h"
16 #include "end.h"
17 #include "initfile.h"
18 #include "item-name.h"
19 #include "item-status-flag-type.h"
20 #include "items.h"
21 #include "libutil.h"
22 #include "los.h"
23 #include "macro.h"
24 #include "maps.h"
25 #include "menu.h"
26 #include "message.h"
27 #include "mgen-data.h"
28 #include "mon-death.h"
29 #include "mon-pick.h"
30 #include "mon-tentacle.h"
31 #include "newgame-def.h"
32 #include "ng-init.h"
33 #include "spl-miscast.h"
34 #include "state.h"
35 #include "stringutil.h"
36 #include "teleport.h"
37 #include "terrain.h"
38 #ifdef USE_TILE
39  #include "tileview.h"
40 #endif
41 #include "unicode.h"
42 #include "unique-creature-list-type.h"
43 #include "version.h"
44 #include "view.h"
45 #include "ui.h"
46 
47 using namespace ui;
48 
49 #define ARENA_VERBOSE
50 
51 namespace msg
52 {
53     // wrap a message tee around a file ptr, which can be null.
54     // for a more general purpose application you'd want this to handle opening
55     // and closing the file too, but that would require some restructuring of the
56     // arena.
57     class arena_tee : tee
58     {
59     public:
arena_tee(FILE ** _file)60         arena_tee(FILE **_file) : tee(), file(_file) { }
61 
~arena_tee()62         ~arena_tee()
63         {
64             if (*file)
65                 fflush(*file);
66         }
67 
append(const string & s,msg_channel_type ch=MSGCH_PLAIN)68         void append(const string &s, msg_channel_type ch = MSGCH_PLAIN)
69         {
70             if (Options.arena_dump_msgs && *file)
71             {
72                 if (!s.size())
73                     return;
74                 string prefix;
75                 switch (ch)
76                 {
77                     case MSGCH_DIAGNOSTICS:
78                         prefix = "DIAG: ";
79                         if (Options.arena_dump_msgs_all)
80                             break;
81                         return;
82 
83                     // Ignore messages generated while the user examines
84                     // the arnea.
85                     case MSGCH_PROMPT:
86                     case MSGCH_MONSTER_TARGET:
87                     case MSGCH_FLOOR_ITEMS:
88                     case MSGCH_EXAMINE:
89                     case MSGCH_EXAMINE_FILTER:
90                         return;
91 
92                     // If a monster-damage message ends with '!' it's a
93                     // death message, otherwise it's an examination message
94                     // and should be skipped.
95                     case MSGCH_MONSTER_DAMAGE:
96                         if (s[s.size() - 1] != '!')
97                             return;
98                         break;
99 
100                     case MSGCH_ERROR: prefix = "ERROR: "; break;
101                     case MSGCH_WARN: prefix = "WARN: "; break;
102                     case MSGCH_SOUND: prefix = "SOUND: "; break;
103 
104                     case MSGCH_TALK_VISUAL:
105                     case MSGCH_TALK: prefix = "TALK: "; break;
106                     default: break;
107                 }
108                 formatted_string fs = formatted_string::parse_string(s);
109                 fprintf(*file, "%s%s", prefix.c_str(), fs.tostring().c_str());
110                 fflush(*file);
111             }
112         }
113 
114     private:
115         FILE **file;
116     };
117 }
118 
119 extern void world_reacts();
120 
_results_popup(string msg,bool error=false)121 static void _results_popup(string msg, bool error=false)
122 {
123     // TODO: shared code here with end.cc
124     linebreak_string(msg, 79);
125 
126 #ifdef USE_TILE_WEB
127     tiles_crt_popup show_as_popup;
128     tiles.set_ui_state(UI_CRT);
129 #endif
130 
131     if (error)
132     {
133         msg = string("Arena error:\n\n<lightred>")
134                        + replace_all(msg, "<", "<<");
135         msg += "</lightred>";
136     }
137     else
138         msg = string("Arena results:\n\n") + msg;
139 
140     msg += "\n\n<cyan>Hit any key to continue, "
141                  "ctrl-p for the full log.</cyan>";
142 
143     auto prompt_ui = make_shared<Text>(
144             formatted_string::parse_string(msg));
145     bool done = false;
146     prompt_ui->on_hotkey_event([&](const KeyEvent& ev) {
147         if (ev.key() == CONTROL('P'))
148             replay_messages();
149         else
150             done = true;
151         return done;
152     });
153 
154     mouse_control mc(MOUSE_MODE_MORE);
155     auto popup = make_shared<ui::Popup>(prompt_ui);
156     ui::run_layout(move(popup), done);
157 }
158 
159 namespace arena
160 {
161     static bool skipped_arena_ui = true; // whether this is an interactive session
162     static void write_error(const string &error);
163 
164     struct arena_error : public runtime_error
165     {
arena_errorarena::arena_error166         explicit arena_error(const string &msg, bool _fatal=true)
167             : runtime_error(msg), fatal(_fatal) {}
arena_errorarena::arena_error168         explicit arena_error(const char *msg, bool _fatal=true)
169             : runtime_error(msg), fatal(_fatal) {}
170         bool fatal;
171     };
172 #define arena_error_f(...) arena_error(make_stringf(__VA_ARGS__))
173 #define arena_error_nonfatal_f(...) arena_error(make_stringf(__VA_ARGS__), false)
174 
175     // A faction is just a big list of monsters. Monsters will be dropped
176     // around the appropriate marker.
177     struct faction
178     {
179         string desc;
180         mons_list   members;
181         bool        friendly;
182         int         active_members;
183         bool        won;
184 
185         vector<int>       respawn_list;
186         vector<coord_def> respawn_pos;
187 
factionarena::faction188         faction(bool fr) : members(), friendly(fr), active_members(0),
189                            won(false) { }
190 
191         void place_at(const coord_def &pos);
192 
resetarena::faction193         void reset()
194         {
195             active_members = 0;
196             won            = false;
197 
198             respawn_list.clear();
199             respawn_pos.clear();
200         }
201 
cleararena::faction202         void clear()
203         {
204             reset();
205             members.clear();
206         }
207     };
208 
209     static string teams;
210 
211     static int total_trials = 0;
212 
213     static bool contest_cancelled = false;
214 
215     static bool is_respawning = false;
216 
217     static int trials_done = 0;
218     static int team_a_wins = 0;
219     static int ties        = 0;
220 
221     static int turns       = 0;
222 
223     static bool allow_summons       = true;
224     static bool allow_animate       = true;
225     static bool allow_chain_summons = true;
226     static bool allow_zero_xp       = false;
227     static bool allow_immobile      = true;
228     static bool allow_bands         = true;
229     static bool name_monsters       = false;
230     static bool random_uniques      = false;
231     static bool real_summons        = false;
232     static bool move_summons        = false;
233     static bool respawn             = false;
234     static bool move_respawns       = false;
235 
236     static bool miscasts            = false;
237 
238     static int  summon_throttle     = INT_MAX;
239 
240     static vector<monster_type> uniques_list;
241     static vector<int> a_spawners;
242     static vector<int> b_spawners;
243     static int8_t           to_respawn[MAX_MONSTERS];
244 
245     static int item_drop_times[MAX_ITEMS];
246 
247     static bool banned_glyphs[128];
248 
249     static string arena_type = "";
250     static faction faction_a(true);
251     static faction faction_b(false);
252     static coord_def place_a, place_b;
253 
254     static bool cycle_random     = false;
255     static uint32_t cycle_random_pos = 0;
256 
257     static FILE *file = nullptr;
258     static level_id place(BRANCH_DEPTHS, 1);
259     static string arena_log;
260 
adjust_spells(monster * mons,bool no_summons,bool no_animate)261     static void adjust_spells(monster* mons, bool no_summons, bool no_animate)
262     {
263         monster_spells &spells(mons->spells);
264         erase_if(spells, [&](const mon_spell_slot &t) {
265             return (no_summons && spell_typematch(t.spell, spschool::summoning))
266                 || (no_animate && t.spell == SPELL_ANIMATE_DEAD);
267         });
268     }
269 
adjust_monsters()270     static void adjust_monsters()
271     {
272         for (monster_iterator mon; mon; ++mon)
273         {
274             const bool friendly = mon->friendly();
275             // Set target to the opposite faction's home base.
276             mon->target = friendly ? place_b : place_a;
277         }
278     }
279 
list_eq(const monster * mon)280     static void list_eq(const monster *mon)
281     {
282         if (!Options.arena_list_eq || file == nullptr)
283             return;
284 
285         vector<int> items;
286         for (short it : mon->inv)
287             if (it != NON_ITEM)
288                 items.push_back(it);
289 
290         if (items.empty())
291             return;
292 
293         fprintf(file, "%s:\n", mon->name(DESC_PLAIN, true).c_str());
294 
295         for (int iidx : items)
296         {
297             item_def &item = env.item[iidx];
298             fprintf(file, "        %s\n",
299                     item.name(DESC_PLAIN, false, true).c_str());
300         }
301     }
302 
place_at(const coord_def & pos)303     void faction::place_at(const coord_def &pos)
304     {
305         ASSERT_IN_BOUNDS(pos);
306         for (int i = 0, size = members.size(); i < size; ++i)
307         {
308             mons_spec spec = members.get_monster(i);
309 
310             if (friendly)
311                 spec.attitude = ATT_FRIENDLY;
312 
313             for (int q = 0; q < spec.quantity; ++q)
314             {
315                 const coord_def loc = pos;
316                 if (!in_bounds(loc))
317                     break;
318 
319                 const monster* mon;
320                 if (mons_class_requires_band(spec.type) && !spec.band)
321                 {
322                     unwind_bool no(allow_bands, false);
323                     unwind_bool yes(spec.band, true);
324                     mon = dgn_place_monster(spec, loc, false, true, false);
325                 }
326                 else
327                     mon = dgn_place_monster(spec, loc, false, true, false);
328                 if (!mon)
329                 {
330                     game_ended_with_error(
331                         make_stringf(
332                             "Failed to create monster at (%d,%d) env.grid: %s",
333                             loc.x, loc.y, dungeon_feature_name(env.grid(loc))));
334                 }
335                 list_eq(mon);
336                 to_respawn[mon->mindex()] = i;
337             }
338         }
339     }
340 
center_print(unsigned sz,string text,int number=-1)341     static void center_print(unsigned sz, string text, int number = -1)
342     {
343         if (number >= 0)
344             text = make_stringf("(%d) %s", number, text.c_str());
345 
346         unsigned len = strwidth(text);
347         if (len > sz)
348             text = chop_string(text, len = sz);
349 
350         cprintf("%s%s", string((sz - len) / 2, ' ').c_str(), text.c_str());
351     }
352 
setup_level()353     static void setup_level()
354     {
355         turns = 0;
356 
357         a_spawners.clear();
358         b_spawners.clear();
359         memset(item_drop_times, 0, sizeof(item_drop_times));
360 
361         if (place.is_valid())
362         {
363             you.where_are_you = place.branch;
364             you.depth         = place.depth;
365         }
366 
367         dgn_reset_level();
368 
369         for (int x = 0; x < GXM; ++x)
370             for (int y = 0; y < GYM; ++y)
371                 env.grid[x][y] = DNGN_ROCK_WALL;
372 
373         unwind_bool gen(crawl_state.generating_level, true);
374 
375         typedef unwind_var< set<string> > unwind_stringset;
376 
377         const unwind_stringset mtags(you.uniq_map_tags);
378         const unwind_stringset mnames(you.uniq_map_names);
379 
380         string map_name = "arena_" + arena_type;
381         const map_def *map = random_map_for_tag(map_name);
382 
383         if (!map)
384         {
385             throw arena_error_f("No arena maps named \"%s\"",
386                                 arena_type.c_str());
387         }
388 
389 #ifdef USE_TILE
390         // Arena is never saved, so we can skip this.
391         tile_init_default_flavour();
392         tile_clear_flavour();
393 #endif
394 
395         ASSERT(map);
396         bool success = dgn_place_map(map, false, true);
397         if (!success)
398         {
399             throw arena_error_f("Failed to create arena named \"%s\"",
400                                 arena_type.c_str());
401         }
402         link_items();
403 
404         if (!env.rock_colour)
405             env.rock_colour = CYAN;
406         if (!env.floor_colour)
407             env.floor_colour = LIGHTGREY;
408 
409 #ifdef USE_TILE
410         tile_new_level(true);
411 #endif
412         los_changed();
413         env.markers.activate_all();
414     }
415 
find_monster_spec()416     static string find_monster_spec()
417     {
418         if (!teams.empty())
419             return teams;
420         else
421             return "random v random";
422     }
423 
424     /// @throws arena_error if a monster specification is invalid.
parse_faction(faction & fact,string spec)425     static void parse_faction(faction &fact, string spec)
426     {
427         fact.clear();
428         fact.desc = spec;
429 
430         for (const string &monster : split_string(",", spec))
431         {
432             const string err = fact.members.add_mons(monster, false);
433             if (!err.empty())
434                 throw arena_error(err, false);
435         }
436     }
437 
438     /// @throws arena_error if the monster specification is invalid.
parse_monster_spec()439     static void parse_monster_spec()
440     {
441         string spec = find_monster_spec();
442 
443         allow_chain_summons = !strip_tag(spec, "no_chain_summons");
444 
445         allow_summons   = !strip_tag(spec, "no_summons");
446         allow_animate   = !strip_tag(spec, "no_animate");
447         allow_immobile  = !strip_tag(spec, "no_immobile");
448         allow_bands     = !strip_tag(spec, "no_bands");
449         allow_zero_xp   =  strip_tag(spec, "allow_zero_xp");
450         real_summons    =  strip_tag(spec, "real_summons");
451         move_summons    =  strip_tag(spec, "move_summons");
452         miscasts        =  strip_tag(spec, "miscasts");
453         respawn         =  strip_tag(spec, "respawn");
454         move_respawns   =  strip_tag(spec, "move_respawns");
455         summon_throttle = strip_number_tag(spec, "summon_throttle:");
456 
457         if (real_summons && respawn)
458         {
459             throw arena_error("Can't set real_summons and respawn at same time.",
460                                     false);
461         }
462 
463         if (summon_throttle <= 0)
464             summon_throttle = INT_MAX;
465 
466         cycle_random   = strip_tag(spec, "cycle_random");
467         name_monsters  = strip_tag(spec, "names");
468         random_uniques = strip_tag(spec, "random_uniques");
469 
470         const int ntrials = strip_number_tag(spec, "t:");
471         if (ntrials != TAG_UNFOUND && ntrials >= 1 && ntrials <= 99
472             && !total_trials)
473         {
474             total_trials = ntrials;
475         }
476 
477         arena_type = strip_tag_prefix(spec, "arena:");
478 
479         if (arena_type.empty())
480             arena_type = "default";
481 
482         const int arena_delay = strip_number_tag(spec, "delay:");
483         if (arena_delay >= 0 && arena_delay < 2000)
484             Options.view_delay = arena_delay;
485 
486         string arena_place = strip_tag_prefix(spec, "arena_place:");
487         if (!arena_place.empty())
488         {
489             try
490             {
491                 place = level_id::parse_level_id(arena_place);
492             }
493             catch (const bad_level_id &err)
494             {
495                 throw arena_error_nonfatal_f("Bad place '%s': %s",
496                                     arena_place.c_str(),
497                                     err.what());
498             }
499         }
500 
501         for (unsigned char gly : strip_tag_prefix(spec, "ban_glyphs:"))
502             if (gly < ARRAYSZ(banned_glyphs))
503                 banned_glyphs[gly] = true;
504 
505         vector<string> factions = split_string(" v ", spec);
506 
507         if (factions.size() == 1)
508             factions = split_string(" vs ", spec);
509 
510         if (factions.size() != 2)
511         {
512             throw arena_error_nonfatal_f(
513                                 "Expected arena monster spec \"xxx v yyy\", "
514                                 "but got \"%s\"", spec.c_str());
515         }
516 
517         try
518         {
519             parse_faction(faction_a, factions[0]);
520             parse_faction(faction_b, factions[1]);
521         }
522         catch (const arena_error &err)
523         {
524             throw arena_error_nonfatal_f("Bad monster spec \"%s\": %s",
525                                 spec.c_str(),
526                                 err.what());
527         }
528 
529         if (faction_a.desc == faction_b.desc)
530         {
531             faction_a.desc += " (A)";
532             faction_b.desc += " (B)";
533         }
534     }
535 
setup_monsters()536     static void setup_monsters()
537     {
538         faction_a.reset();
539         faction_b.reset();
540 
541         for (int i = 0; i < MAX_MONSTERS; i++)
542             to_respawn[i] = -1;
543 
544         unwind_var<unique_creature_list> uniq(you.unique_creatures);
545 
546         place_a = dgn_find_feature_marker(DNGN_STONE_STAIRS_UP_I);
547         place_b = dgn_find_feature_marker(DNGN_STONE_STAIRS_DOWN_I);
548 
549         // Place the different factions in different orders on
550         // alternating rounds so that one side doesn't get the
551         // first-move advantage for all rounds.
552         if (trials_done & 1)
553         {
554             faction_a.place_at(place_a);
555             faction_b.place_at(place_b);
556         }
557         else
558         {
559             faction_b.place_at(place_b);
560             faction_a.place_at(place_a);
561         }
562 
563         adjust_monsters();
564     }
565 
show_fight_banner(bool after_fight=false)566     static void show_fight_banner(bool after_fight = false)
567     {
568         int line = 1;
569 
570         cgotoxy(1, line++, GOTO_STAT);
571         textcolour(WHITE);
572         center_print(crawl_view.hudsz.x, string("Crawl ") + Version::Long);
573         line++;
574 
575         cgotoxy(1, line++, GOTO_STAT);
576         textcolour(YELLOW);
577         center_print(crawl_view.hudsz.x, faction_a.desc,
578                      total_trials ? team_a_wins : -1);
579         cgotoxy(1, line++, GOTO_STAT);
580         textcolour(LIGHTGREY);
581         center_print(crawl_view.hudsz.x, "vs");
582         cgotoxy(1, line++, GOTO_STAT);
583         textcolour(YELLOW);
584         center_print(crawl_view.hudsz.x, faction_b.desc,
585                      total_trials ? trials_done - team_a_wins - ties : -1);
586 
587         if (total_trials > 1 && trials_done < total_trials)
588         {
589             cgotoxy(1, line++, GOTO_STAT);
590             textcolour(BROWN);
591             center_print(crawl_view.hudsz.x,
592                          make_stringf("Round %d of %d",
593                                       after_fight ? trials_done
594                                                   : trials_done + 1,
595                                       total_trials));
596         }
597         else
598         {
599             cgotoxy(1, line++, GOTO_STAT);
600             textcolour(BROWN);
601             clear_to_end_of_line();
602         }
603     }
604 
setup_others()605     static void setup_others()
606     {
607         you.species = SP_HUMAN;
608         you.char_class = JOB_FIGHTER;
609         you.experience_level = 27;
610 
611         you.position.y = -1;
612         coord_def yplace(dgn_find_feature_marker(DNGN_ESCAPE_HATCH_UP));
613         crawl_view.set_player_at(yplace);
614 
615         you.mutation[MUT_ACUTE_VISION] = 3;
616 
617         you.your_name = "Arena";
618 
619         you.hp = you.hp_max = 99;
620 
621         for (int i = 0; i < NUM_STATS; ++i)
622             you.base_stats[i] = 20;
623 
624         // XXX: now that you.species is valid, do a layout.
625         // This is necessary to ensure that the stat window is positioned.
626 #ifdef USE_TILE
627         tiles.resize();
628 #endif
629 
630         show_fight_banner();
631     }
632 
expand_mlist(int exp)633     static void expand_mlist(int exp)
634     {
635         crawl_view.mlistp.y  -= exp;
636         crawl_view.mlistsz.y += exp;
637     }
638 
639     /// @throws arena_error if the specification was invalid.
setup_fight()640     static void setup_fight()
641     {
642         //msg::suppress mx;
643         parse_monster_spec();
644         setup_level();
645 
646         // Monster setup may block waiting for matchups.
647         setup_monsters();
648 
649         setup_others();
650     }
651 
count_foes()652     static void count_foes()
653     {
654         int orig_a = faction_a.active_members;
655         int orig_b = faction_b.active_members;
656 
657         if (orig_a < 0)
658             mprf(MSGCH_ERROR, "Book-keeping says faction_a has negative active members.");
659 
660         if (orig_b < 0)
661             mprf(MSGCH_ERROR, "Book-keeping says faction_b has negative active members.");
662 
663         faction_a.active_members = 0;
664         faction_b.active_members = 0;
665 
666         for (monster_iterator mons; mons; ++mons)
667         {
668             if (mons_is_tentacle_or_tentacle_segment(mons->type))
669                 continue;
670             if (mons->attitude == ATT_FRIENDLY)
671                 faction_a.active_members++;
672             else if (mons->attitude == ATT_HOSTILE)
673                 faction_b.active_members++;
674         }
675 
676         if (orig_a != faction_a.active_members
677             || orig_b != faction_b.active_members)
678         {
679             mprf(MSGCH_ERROR, "Book-keeping error in faction member count: "
680                               "%d:%d instead of %d:%d",
681                               orig_a, orig_b,
682                               faction_a.active_members, faction_b.active_members);
683 
684             if (faction_a.active_members > 0
685                 && faction_b.active_members <= 0)
686             {
687                 faction_a.won = true;
688                 faction_b.won = false;
689             }
690             else if (faction_b.active_members > 0
691                      && faction_a.active_members <= 0)
692             {
693                 faction_b.won = true;
694                 faction_a.won = false;
695             }
696         }
697     }
698 
699     // Returns true as long as at least one member of each faction is alive.
fight_is_on()700     static bool fight_is_on()
701     {
702         if (faction_a.active_members > 0 && faction_b.active_members > 0)
703         {
704             if (faction_a.won || faction_b.won)
705             {
706                 mprf(MSGCH_ERROR, "Both factions alive but one declared the winner.");
707                 faction_a.won = false;
708                 faction_b.won = false;
709             }
710             return true;
711         }
712 
713         // Sync up our book-keeping with the actual state, and report
714         // any inconsistencies.
715         count_foes();
716 
717         return faction_a.active_members > 0 && faction_b.active_members > 0;
718     }
719 
720     // Try to prevent random luck from letting one spawner fill up the
721     // arena with so many monsters that the other spawner can never get
722     // back on even footing.
balance_spawners()723     static void balance_spawners()
724     {
725         if (a_spawners.empty() || b_spawners.empty())
726             return;
727 
728         if (faction_a.active_members == 0 || faction_b.active_members == 0)
729         {
730             mprf(MSGCH_ERROR, "ERROR: Both sides have spawners, but the active "
731                  "member count of one side has been reduced to zero!");
732             return;
733         }
734 
735         for (int idx : a_spawners)
736         {
737             env.mons[idx].speed_increment *= faction_b.active_members;
738             env.mons[idx].speed_increment /= faction_a.active_members;
739         }
740 
741         for (int idx : b_spawners)
742         {
743             env.mons[idx].speed_increment *= faction_a.active_members;
744             env.mons[idx].speed_increment /= faction_b.active_members;
745         }
746     }
747 
do_miscasts()748     static void do_miscasts()
749     {
750         if (!miscasts)
751             return;
752 
753         for (monster_iterator mon; mon; ++mon)
754         {
755             if (mon->type == MONS_TEST_SPAWNER)
756                 continue;
757 
758             if (!mon->alive())
759                 continue;
760 
761             miscast_effect(**mon, *mon, {miscast_source::wizard},
762                            spschool::random, random_range(1, 9),
763                            random_range(1, 100), "arena miscast");
764         }
765     }
766 
handle_keypress(int ch)767     static void handle_keypress(int ch)
768     {
769         if (key_is_escape(ch) || toalower(ch) == 'q')
770         {
771             contest_cancelled = true;
772             return;
773         }
774 
775         const command_type cmd = key_to_command(ch, KMC_DEFAULT);
776 
777         // We only allow a short list of commands to be used in the arena.
778         switch (cmd)
779         {
780         case CMD_LOOK_AROUND:
781         case CMD_SUSPEND_GAME:
782         case CMD_REPLAY_MESSAGES:
783             break;
784 
785         default:
786             return;
787         }
788 
789         if (file != nullptr)
790             fflush(file);
791 
792         cursor_control coff(true);
793 
794         unwind_var<game_type> type(crawl_state.type, GAME_TYPE_NORMAL);
795         unwind_bool ar_susp(crawl_state.arena_suspended, true);
796         coord_def yplace(dgn_find_feature_marker(DNGN_ESCAPE_HATCH_UP));
797         unwind_var<coord_def> pos(you.position);
798         you.position = yplace;
799         process_command(cmd);
800     }
801 
do_respawn(faction & fac)802     static void do_respawn(faction &fac)
803     {
804         is_respawning = true;
805         for (unsigned int _i = fac.respawn_list.size(); _i > 0; _i--)
806         {
807             unsigned int i = _i - 1;
808 
809             coord_def pos      = fac.respawn_pos[i];
810             int       spec_idx = fac.respawn_list[i];
811             mons_spec spec     = fac.members.get_monster(spec_idx);
812 
813             if (fac.friendly)
814                 spec.attitude = ATT_FRIENDLY;
815 
816             monster *mon = dgn_place_monster(spec, pos, false, true);
817 
818             if (!mon && fac.active_members == 0 && monster_at(pos))
819             {
820                 // We have no members left, so to prevent the round
821                 // from ending attempt to displace whatever is in
822                 // our position.
823                 monster& other = *monster_at(pos);
824 
825                 if (to_respawn[other.mindex()] == -1)
826                 {
827                     // The other monster isn't a respawner itself, so
828                     // just get rid of it.
829                     mprf(MSGCH_DIAGNOSTICS,
830                          "Dismissing non-respawner %s to make room for "
831                          "respawner whose side has 0 active members.",
832                          other.name(DESC_PLAIN, true).c_str());
833                     monster_die(other, KILL_DISMISSED, NON_MONSTER);
834                 }
835                 else
836                 {
837                     // Other monster is a respawner, try to move it.
838                     mprf(MSGCH_DIAGNOSTICS,
839                          "Teleporting respawner %s to make room for "
840                          "other respawner whose side has 0 active members.",
841                          other.name(DESC_PLAIN, true).c_str());
842                     monster_teleport(&other, true);
843                 }
844 
845                 mon = dgn_place_monster(spec, pos, false, true);
846             }
847 
848             if (mon)
849             {
850                 // We succeeded, so remove from list.
851                 fac.respawn_list.erase(fac.respawn_list.begin() + i);
852                 fac.respawn_pos.erase(fac.respawn_pos.begin() + i);
853 
854                 to_respawn[mon->mindex()] = spec_idx;
855 
856                 if (move_respawns)
857                     monster_teleport(mon, true, true);
858             }
859             else
860             {
861                 // Couldn't respawn, so leave it on the list; hopefully
862                 // space will open up later.
863             }
864         }
865         is_respawning = false;
866     }
867 
do_fight()868     static void do_fight()
869     {
870         viewwindow();
871         update_screen();
872         clear_messages(true);
873 
874         {
875             cursor_control coff(false);
876             while (fight_is_on() && !contest_cancelled)
877             {
878 #ifdef ARENA_VERBOSE
879                 mprf("---- Turn #%d ----", turns);
880 #endif
881 
882                 // Check the consistency of our book-keeping every 100 turns.
883                 if ((turns++ % 100) == 0)
884                     count_foes();
885 
886                 you.time_taken = 10;
887                 //report_foes();
888                 world_reacts();
889                 do_miscasts();
890                 do_respawn(faction_a);
891                 do_respawn(faction_b);
892                 balance_spawners();
893                 ui::delay(Options.view_delay);
894                 clear_messages();
895                 ASSERT(you.pet_target == MHITNOT);
896             }
897             viewwindow();
898             update_screen();
899         }
900 
901         if (contest_cancelled)
902         {
903             mpr("Cancelled contest at user request");
904             ui::delay(Options.view_delay);
905             clear_messages();
906             return;
907         }
908 
909         clear_messages();
910 
911         trials_done++;
912 
913         // We bother with all this to properly deal with ties, and with
914         // ball lightning or ballistomycete spores winning the fight via suicide.
915         // The sanity checking is probably just paranoia.
916         bool was_tied = false;
917         if (!faction_a.won && !faction_b.won)
918         {
919             if (faction_a.active_members > 0)
920             {
921                 mprf(MSGCH_ERROR, "Tie declared, but faction_a won.");
922                 team_a_wins++;
923                 faction_a.won = true;
924             }
925             else if (faction_b.active_members > 0)
926             {
927                 mprf(MSGCH_ERROR, "Tie declared, but faction_b won.");
928                 faction_b.won = true;
929             }
930             else
931             {
932                 ties++;
933                 was_tied = true;
934             }
935         }
936         else if (faction_a.won && faction_b.won)
937         {
938             faction_a.won = false;
939             faction_b.won = false;
940 
941             mprf(MSGCH_ERROR, "*BOTH* factions won?!");
942             if (faction_a.active_members > 0)
943             {
944                 mprf(MSGCH_ERROR, "Faction_a real winner.");
945                 team_a_wins++;
946                 faction_a.won = true;
947             }
948             else if (faction_b.active_members > 0)
949             {
950                 mprf(MSGCH_ERROR, "Faction_b real winner.");
951                 faction_b.won = true;
952             }
953             else
954             {
955                 mprf(MSGCH_ERROR, "Both sides dead.");
956                 ties++;
957                 was_tied = true;
958             }
959         }
960         else if (faction_a.won)
961             team_a_wins++;
962 
963         show_fight_banner(true);
964 
965         string msg;
966         if (was_tied)
967             msg = "Tie";
968         else
969             msg = "Winner: %s!";
970 
971         if (Options.arena_dump_msgs || Options.arena_list_eq)
972             msg = "---------- " + msg + " ----------";
973 
974         if (was_tied)
975             mpr(msg);
976         else
977             mprf(msg.c_str(),
978                  faction_a.won ? faction_a.desc.c_str()
979                                : faction_b.desc.c_str());
980     }
981 
global_setup(const string & arena_teams)982     static void global_setup(const string& arena_teams)
983     {
984         // Clear some things that shouldn't persist across restart_after_game.
985         // parse_monster_spec and setup_fight will clear the rest.
986         total_trials = trials_done = team_a_wins = ties = 0;
987         contest_cancelled = false;
988         is_respawning = false;
989         uniques_list.clear();
990         memset(banned_glyphs, 0, sizeof(banned_glyphs));
991         arena_type = "";
992         place = level_id(BRANCH_DEPTHS, 1);
993         arena_log = "";
994 
995         // [ds] Turning off view_lock crashes arena.
996         Options.view_lock_x = Options.view_lock_y = true;
997 
998         teams = arena_teams;
999         // Set various options from the arena spec's tags
1000         parse_monster_spec(); // may throw an arena_error
1001 
1002         crawl_view.init_geometry();
1003         expand_mlist(5);
1004 
1005         for (monster_type i = MONS_0; i < NUM_MONSTERS; ++i)
1006         {
1007             if (i == MONS_PLAYER_GHOST)
1008                 continue;
1009 
1010             if (mons_is_unique(i) && !arena_veto_random_monster(i))
1011                 uniques_list.push_back(i);
1012         }
1013     }
1014 
global_shutdown()1015     static void global_shutdown()
1016     {
1017         if (file != nullptr)
1018             fclose(file);
1019 
1020         file = nullptr;
1021         arena_log = "";
1022     }
1023 
write_results()1024     static void write_results()
1025     {
1026         if (file != nullptr)
1027         {
1028             if (Options.arena_dump_msgs || Options.arena_list_eq)
1029                 fprintf(file, "========================================\n");
1030             fprintf(file, "%d-%d", team_a_wins,
1031                     trials_done - team_a_wins - ties);
1032             if (ties > 0)
1033                 fprintf(file, "-%d", ties);
1034             fprintf(file, "\n");
1035         }
1036     }
1037 
write_error(const string & error)1038     static void write_error(const string &error)
1039     {
1040         if (file != nullptr)
1041         {
1042             fprintf(file, "err: %s\n", error.c_str());
1043             fclose(file);
1044         }
1045         file = nullptr;
1046     }
1047 
simulate()1048     static void simulate()
1049     {
1050         init_level_connectivity();
1051 
1052         class UIArena : public Box
1053         {
1054         public:
1055             UIArena() : Box(Widget::VERT) {
1056                 expand_h = expand_v = true;
1057             };
1058             virtual void _render() override {};
1059             virtual void _allocate_region() override {
1060                 show_fight_banner();
1061                 viewwindow();
1062                 update_screen();
1063                 display_message_window();
1064             };
1065             virtual bool on_event(const Event& ev) override {
1066                 if (ev.type() != Event::Type::KeyDown)
1067                     return false;
1068                 handle_keypress(static_cast<const KeyEvent&>(ev).key());
1069                 ASSERT(crawl_state.game_is_arena());
1070                 ASSERT(!crawl_state.arena_suspended);
1071                 return true;
1072             };
1073         };
1074 
1075         auto ui = make_shared<UIArena>();
1076         ui::push_layout(ui);
1077 
1078         do
1079         {
1080             try
1081             {
1082                 setup_fight();
1083             }
1084             catch (const arena_error &error)
1085             {
1086                 write_error(error.what());
1087                 game_ended_with_error(error.what());
1088                 continue;
1089             }
1090             do_fight();
1091 
1092             if (trials_done < total_trials)
1093                 ui::delay(Options.view_delay * 5);
1094         }
1095         while (!contest_cancelled && trials_done < total_trials);
1096 
1097         ui::delay(Options.view_delay * 5);
1098 
1099         if (total_trials > 0)
1100         {
1101             string outcome = make_stringf(
1102                 "Final score: %s (%d); %s (%d) [%d ties]",
1103                  faction_a.desc.c_str(), team_a_wins,
1104                  faction_b.desc.c_str(), trials_done - team_a_wins - ties,
1105                  ties);
1106             mpr(outcome);
1107             if (!skipped_arena_ui)
1108                 _results_popup(outcome);
1109         }
1110 
1111         ui::pop_layout();
1112 
1113         write_results();
1114     }
1115 }
1116 
1117 /////////////////////////////////////////////////////////////////////////////
1118 
1119 // Various arena callbacks
1120 
arena_pick_random_monster(const level_id & place)1121 monster_type arena_pick_random_monster(const level_id &place)
1122 {
1123     if (arena::random_uniques)
1124     {
1125         const vector<monster_type> &uniques = arena::uniques_list;
1126 
1127         const monster_type type = uniques[random2(uniques.size())];
1128         you.unique_creatures.set(type, false);
1129 
1130         return type;
1131     }
1132 
1133     if (!arena::cycle_random)
1134         return RANDOM_MONSTER;
1135 
1136     for (int tries = 0; tries <= NUM_MONSTERS; tries++)
1137     {
1138         monster_type mons = pick_monster_by_hash(place.branch,
1139             ++arena::cycle_random_pos);
1140 
1141         if (arena_veto_random_monster(mons))
1142             continue;
1143 
1144         return mons;
1145     }
1146 
1147     game_ended_with_error(
1148         make_stringf("No random monsters for place '%s'",
1149                      arena::place.describe().c_str()));
1150 }
1151 
arena_veto_random_monster(monster_type type)1152 bool arena_veto_random_monster(monster_type type)
1153 {
1154     if (mons_is_tentacle_or_tentacle_segment(type))
1155         return true;
1156     if (!arena::allow_immobile && mons_class_is_stationary(type))
1157         return true;
1158     if (!arena::allow_zero_xp && !mons_class_gives_xp(type))
1159         return true;
1160     if (!(mons_char(type) & ~127) && arena::banned_glyphs[mons_char(type)])
1161         return true;
1162 
1163     return false;
1164 }
1165 
arena_veto_place_monster(const mgen_data & mg,bool first_band_member,const coord_def & pos)1166 bool arena_veto_place_monster(const mgen_data &mg, bool first_band_member,
1167                               const coord_def& pos)
1168 {
1169     UNUSED(pos);
1170 
1171     // If the first band member makes it past the summon throttle cut,
1172     // let all of the rest of its band in too regardless of the summon
1173     // throttle.
1174     if (mg.abjuration_duration > 0 && first_band_member)
1175     {
1176         if (mg.behaviour == BEH_FRIENDLY
1177             && arena::faction_a.active_members > arena::summon_throttle)
1178         {
1179             return true;
1180         }
1181         else if (mg.behaviour == BEH_HOSTILE
1182                  && arena::faction_b.active_members > arena::summon_throttle)
1183         {
1184             return true;
1185         }
1186 
1187     }
1188     return !arena::allow_bands && !first_band_member
1189            || !(mons_char(mg.cls) & ~127)
1190               && arena::banned_glyphs[mons_char(mg.cls)];
1191 }
1192 
1193 // XXX: Still having some trouble with book-keeping if a slime creature
1194 // is placed via splitting.
arena_placed_monster(monster * mons)1195 void arena_placed_monster(monster* mons)
1196 {
1197     if (mons_is_tentacle_or_tentacle_segment(mons->type))
1198         ; // we don't count tentacles or tentacle segments, even free-standing
1199     else if (mons->attitude == ATT_FRIENDLY)
1200     {
1201         arena::faction_a.active_members++;
1202         arena::faction_b.won = false;
1203     }
1204     else if (mons->attitude == ATT_HOSTILE)
1205     {
1206         arena::faction_b.active_members++;
1207         arena::faction_a.won = false;
1208     }
1209 
1210     if (!arena::allow_summons || !arena::allow_animate)
1211     {
1212         arena::adjust_spells(mons, !arena::allow_summons,
1213                              !arena::allow_animate);
1214     }
1215 
1216     if (mons->type == MONS_TEST_SPAWNER)
1217     {
1218         if (mons->attitude == ATT_FRIENDLY)
1219             arena::a_spawners.push_back(mons->mindex());
1220         else if (mons->attitude == ATT_HOSTILE)
1221             arena::b_spawners.push_back(mons->mindex());
1222     }
1223 
1224     const bool summoned = mons->is_summoned();
1225 
1226 #ifdef ARENA_VERBOSE
1227     mprf("%s %s!",
1228          mons->full_name(DESC_A).c_str(),
1229          arena::is_respawning                ? "respawns" :
1230          (summoned && ! arena::real_summons) ? "is summoned"
1231                                              : "enters the arena");
1232 #endif
1233 
1234     for (mon_inv_iterator ii(*mons); ii; ++ii)
1235     {
1236         ii->flags |= ISFLAG_IDENT_MASK;
1237 
1238         // Set the "drop" time here in case the monster drops the
1239         // item without dying, like being polymorphed.
1240         arena::item_drop_times[ii->index()] = arena::turns;
1241     }
1242 
1243     if (arena::name_monsters && !mons->is_named())
1244         mons->mname = make_name();
1245 
1246     if (summoned)
1247     {
1248         // Real summons drop corpses and items.
1249         if (arena::real_summons)
1250         {
1251             mons->del_ench(ENCH_ABJ, true, false);
1252             for (mon_inv_iterator ii(*mons); ii; ++ii)
1253                 ii->flags &= ~ISFLAG_SUMMONED;
1254         }
1255 
1256         if (arena::move_summons)
1257             monster_teleport(mons, true, true);
1258 
1259         if (!arena::allow_chain_summons)
1260             arena::adjust_spells(mons, true, false);
1261     }
1262 }
1263 
1264 // Take care of respawning slime creatures merging and then splitting.
arena_split_monster(monster * split_from,monster * split_to)1265 void arena_split_monster(monster* split_from, monster* split_to)
1266 {
1267     if (!arena::respawn)
1268         return;
1269 
1270     const int from_idx   = split_from->mindex();
1271     const int member_idx = arena::to_respawn[from_idx];
1272 
1273     if (member_idx == -1)
1274         return;
1275 
1276     arena::to_respawn[split_to->mindex()] = member_idx;
1277 }
1278 
arena_monster_died(monster * mons,killer_type killer,int killer_index,bool silent,const item_def * corpse)1279 void arena_monster_died(monster* mons, killer_type killer,
1280                         int killer_index, bool silent, const item_def* corpse)
1281 {
1282     if (mons_is_tentacle_or_tentacle_segment(mons->type))
1283         ; // part of a monster, or a spell
1284     else if (mons->attitude == ATT_FRIENDLY)
1285         arena::faction_a.active_members--;
1286     else if (mons->attitude == ATT_HOSTILE)
1287         arena::faction_b.active_members--;
1288 
1289     if (arena::faction_a.active_members > 0
1290         && arena::faction_b.active_members <= 0)
1291     {
1292         arena::faction_a.won = true;
1293     }
1294     else if (arena::faction_b.active_members > 0
1295              && arena::faction_a.active_members <= 0)
1296     {
1297         arena::faction_b.won = true;
1298     }
1299     // Everyone is dead. Is it a tie, or something else?
1300     else if (arena::faction_a.active_members <= 0
1301              && arena::faction_b.active_members <= 0)
1302     {
1303         if (mons->flags & MF_HARD_RESET && !MON_KILL(killer))
1304             mpr("Last arena monster was dismissed.");
1305         // If all monsters are dead, and the last one to die is a giant
1306         // spore or ball lightning, then that monster's faction is the
1307         // winner, since self-destruction is their purpose. But if a
1308         // trap causes the spore to explode, and that kills everything,
1309         // it's a tie, since it counts as the trap killing everyone.
1310         else if (mons_self_destructs(*mons) && MON_KILL(killer))
1311         {
1312             if (mons->attitude == ATT_FRIENDLY)
1313                 arena::faction_a.won = true;
1314             else if (mons->attitude == ATT_HOSTILE)
1315                 arena::faction_b.won = true;
1316         }
1317     }
1318 
1319     // Only respawn those monsters which were initially placed in the
1320     // arena.
1321     const int midx = mons->mindex();
1322     if (arena::respawn && arena::to_respawn[midx] != -1
1323         // Don't respawn when a slime 'dies' from merging with another
1324         // slime.
1325         && !(mons->type == MONS_SLIME_CREATURE && silent
1326              && killer == KILL_MISC
1327              && killer_index == NON_MONSTER))
1328     {
1329         arena::faction *fac = nullptr;
1330         if (mons->attitude == ATT_FRIENDLY)
1331             fac = &arena::faction_a;
1332         else if (mons->attitude == ATT_HOSTILE)
1333             fac = &arena::faction_b;
1334 
1335         if (fac)
1336         {
1337             int member_idx = arena::to_respawn[midx];
1338             fac->respawn_list.push_back(member_idx);
1339             fac->respawn_pos.push_back(mons->pos());
1340 
1341             // Un-merge slime when it respawns, but only if it's
1342             // specifically a slime, and not a random monster which
1343             // happens to be a slime.
1344             if (mons->type == MONS_SLIME_CREATURE
1345                 && (fac->members.get_monster(member_idx).type
1346                     == MONS_SLIME_CREATURE))
1347             {
1348                 for (int i = 1; i < mons->blob_size; i++)
1349                 {
1350                     fac->respawn_list.push_back(member_idx);
1351                     fac->respawn_pos.push_back(mons->pos());
1352                 }
1353             }
1354 
1355             arena::to_respawn[midx] = -1;
1356         }
1357     }
1358 
1359     if (corpse)
1360         arena::item_drop_times[corpse->index()] = arena::turns;
1361 
1362     // Won't be dropping any items.
1363     if (mons->flags & MF_HARD_RESET)
1364         return;
1365 
1366     for (mon_inv_iterator ii(*mons); ii; ++ii)
1367     {
1368         if (ii->flags & ISFLAG_SUMMONED)
1369             continue;
1370 
1371         arena::item_drop_times[ii->index()] = arena::turns;
1372     }
1373 }
1374 
_sort_by_age(int a,int b)1375 static bool _sort_by_age(int a, int b)
1376 {
1377     return arena::item_drop_times[a] < arena::item_drop_times[b];
1378 }
1379 
1380 #define DESTROY_ITEM(i) \
1381 { \
1382     destroy_item(i, true); \
1383     arena::item_drop_times[i] = 0; \
1384     cull_count++; \
1385     if (first_avail == NON_ITEM) \
1386         first_avail = i; \
1387 }
1388 
1389 // Culls the items which have been on the floor the longest, culling the
1390 // newest items last. Items which a monster dropped voluntarily or
1391 // because of being polymorphed, rather than because of dying, are
1392 // culled earlier than they should be, but it's not like we have to be
1393 // fair to the arena monsters.
arena_cull_items()1394 int arena_cull_items()
1395 {
1396     vector<int> items;
1397 
1398     int first_avail = NON_ITEM;
1399 
1400     for (int i = 0; i < MAX_ITEMS; i++)
1401     {
1402         // All items in env.item[] are valid when we're called.
1403         const item_def &item(env.item[i]);
1404 
1405         // We want floor items.
1406         if (!in_bounds(item.pos))
1407             continue;
1408 
1409         items.push_back(i);
1410     }
1411 
1412     // Cull half of items on the floor.
1413     const int cull_target = items.size() / 2;
1414     int cull_count  = 0;
1415 
1416     sort(items.begin(), items.end(), _sort_by_age);
1417 
1418     vector<int> ammo;
1419 
1420     for (int idx : items)
1421     {
1422         const item_def &item(env.item[idx]);
1423 
1424         // If the drop time is 0 then this is probably thrown ammo.
1425         if (arena::item_drop_times[idx] == 0)
1426         {
1427             // We know it's at least this old.
1428             arena::item_drop_times[idx] = arena::turns;
1429 
1430             // Arrows/needles/etc on the floor is just clutter.
1431             if (item.base_type != OBJ_MISSILES
1432                || item.sub_type == MI_JAVELIN
1433                || item.sub_type == MI_BOOMERANG
1434                || item.sub_type == MI_THROWING_NET)
1435             {
1436                 ammo.push_back(idx);
1437                 continue;
1438             }
1439         }
1440         DESTROY_ITEM(idx);
1441         if (cull_count >= cull_target)
1442             break;
1443     }
1444 
1445     if (cull_count >= cull_target)
1446     {
1447         dprf("On turn #%d culled %d items dropped by monsters, done.",
1448              arena::turns, cull_count);
1449         return first_avail;
1450     }
1451 
1452     dprf("On turn #%d culled %d items dropped by monsters, culling some more.",
1453          arena::turns, cull_count);
1454 
1455 #ifdef DEBUG_DIAGNOSTICS
1456     const int count1 = cull_count;
1457 #endif
1458     for (int idx : ammo)
1459     {
1460         DESTROY_ITEM(idx);
1461         if (cull_count >= cull_target)
1462             break;
1463     }
1464 
1465     if (cull_count >= cull_target)
1466         dprf("Culled %d (probably) ammo items, done.", cull_count - count1);
1467     else
1468     {
1469         dprf("Culled %d items total, short of target %d.",
1470             cull_count, cull_target);
1471     }
1472     return first_avail;
1473 } // arena_cull_items
1474 
1475 /////////////////////////////////////////////////////////////////////////////
1476 
_init_arena()1477 static void _init_arena()
1478 {
1479     initialise_branch_depths();
1480     run_map_global_preludes();
1481     run_map_local_preludes();
1482     initialise_item_descriptions();
1483 }
1484 
_choose_arena_teams(newgame_def & choice,const string & default_arena_teams)1485 static void _choose_arena_teams(newgame_def& choice,
1486                                 const string &default_arena_teams)
1487 {
1488 #ifdef USE_TILE_WEB
1489     tiles_crt_popup show_as_popup;
1490 #endif
1491 
1492     if (!choice.arena_teams.empty())
1493         return;
1494     arena::skipped_arena_ui = false;
1495     clear_message_store();
1496 
1497     auto vbox = make_shared<Box>(ui::Widget::VERT);
1498     vbox->add_child(make_shared<Text>("Enter your choice of teams:\n "));
1499     vbox->set_cross_alignment(Widget::Align::STRETCH);
1500     auto teams_input = make_shared<ui::TextEntry>();
1501     teams_input->set_sync_id("teams");
1502     teams_input->set_text(default_arena_teams);
1503     vbox->add_child(teams_input);
1504     formatted_string prompt;
1505     prompt.cprintf("\nExamples:\n");
1506     prompt.cprintf("  Sigmund v Jessica\n");
1507     prompt.cprintf("  99 orc v the Royal Jelly\n");
1508     prompt.cprintf("  20-headed hydra v 10 kobold ; scimitar ego:flaming");
1509     vbox->add_child(make_shared<Text>(move(prompt)));
1510 
1511     auto popup = make_shared<ui::Popup>(move(vbox));
1512 
1513     bool done = false, cancel = false;
1514     popup->on_hotkey_event([&](const KeyEvent& ev) {
1515         return done = (ev.key() == CK_ENTER);
1516     });
1517     popup->on_keydown_event([&](const KeyEvent& ev) {
1518         return done = cancel = key_is_escape(ev.key());
1519     });
1520 
1521     ui::run_layout(move(popup), done, teams_input);
1522 
1523     if (cancel || crawl_state.seen_hups)
1524     {
1525         arena::global_shutdown();
1526         game_ended(crawl_state.bypassed_startup_menu
1527                     ? game_exit::death : game_exit::abort);
1528     }
1529     choice.arena_teams = teams_input->get_text();
1530     if (choice.arena_teams.empty())
1531         choice.arena_teams = default_arena_teams;
1532 }
1533 
run_arena(const newgame_def & choice,const string & default_arena_teams)1534 NORETURN void run_arena(const newgame_def& choice, const string &default_arena_teams)
1535 {
1536     ASSERT(crawl_state.game_is_arena());
1537 
1538     newgame_def arena_choice = choice;
1539     string last_teams = default_arena_teams;
1540     if (arena::file != nullptr)
1541         end(0, false, "Results file already open");
1542     // would be more elegant if arena_tee handled file open/close, but
1543     // that would need a bunch of refactoring of how the file is handled here.
1544     arena::file = fopen("arena.result", "w");
1545     msg::arena_tee log(&arena::file);
1546 
1547     do
1548     {
1549         try
1550         {
1551             _choose_arena_teams(arena_choice, last_teams);
1552             write_newgame_options_file(arena_choice);
1553             _init_arena();
1554 
1555             ASSERT(!crawl_state.arena_suspended);
1556 
1557 #ifdef WIZARD
1558             // The player has wizard powers for the duration of the arena.
1559             unwind_bool wiz(you.wizard, true);
1560 #endif
1561 
1562             arena::global_setup(arena_choice.arena_teams);
1563             arena::simulate();
1564             arena::global_shutdown();
1565             game_ended(game_exit::death); // there is only death in the arena
1566         }
1567         catch (const arena::arena_error &error)
1568         {
1569             if (error.fatal || arena::skipped_arena_ui)
1570             {
1571                 arena::write_error(error.what());
1572                 game_ended_with_error(error.what());
1573             }
1574             else
1575             {
1576                 mprf(MSGCH_ERROR, "%s", error.what());
1577                 _results_popup(error.what(), true);
1578                 last_teams = arena_choice.arena_teams;
1579                 arena_choice.arena_teams = "";
1580                 // fallthrough
1581             }
1582         }
1583     }
1584     while (true);
1585 }
1586