Shape text using Pango and HarfBuzz; gives us nice ligatures and exotic scripts.
[ccbs] / bigscreen / groupscreen.cpp
1 #include <cstdio>
2 #include <algorithm>
3 #include <map>
4 #include <assert.h>
5
6 #include "resolution.h"
7 #include "groupscreen.h"
8 #include "fetch_group.h"
9 #include "fetch_max_score_for_songs.h"
10 #include "fetch_max_score_for_players.h"
11 #include "fetch_needs_update.h"
12 #include "fetch_highscore.h"
13 #include "fonts.h"
14 #include "theme.h"
15
16 std::string theme_suffix_from_row(unsigned row)
17 {
18         if (row % 2 == 0) {
19                 return ".odd";  // :-)
20         } else {
21                 return ".even";
22         }
23 }
24
25 GroupScreen::GroupScreen(pqxx::connection &conn, unsigned tournament, unsigned round, unsigned parallel, unsigned machine, unsigned num_machines, unsigned players_per_machine)
26         : tournament(tournament), round(round), parallel(parallel), machine(machine), num_machines(num_machines), players_per_machine(players_per_machine), scores_changed(conn, "scores"), conn(conn), valid(false)
27 {
28 }
29
30 GroupScreen::~GroupScreen()
31 {
32 }
33
34 bool GroupScreen::check_invalidated()
35 {
36         if (!valid)
37                 return true;
38         if (!scores_changed.get_flag())
39                 return false;
40         scores_changed.reset_flag();
41
42         bool needs_update;
43         conn.perform(FetchNeedsUpdate(last_updated, tournament, round, parallel, &needs_update));
44
45         valid = !needs_update;
46         return needs_update;
47 }
48
49 unsigned GroupScreen::get_show_players(const Group &group)
50 {
51         unsigned num_players_this_machine = (group.players.size() + num_machines - machine - 1) / num_machines;
52         return std::min(num_players_this_machine, 9U);
53 }
54
55 void GroupScreen::draw_main_heading(std::vector<TextDefer> &td)
56 {
57         char heading[64];
58         if (num_machines == 1) {
59                 if (parallel == 0) {
60                         std::sprintf(heading, "Round %u", round);
61                 } else {
62                         std::sprintf(heading, "Round %u, Group %u", round, parallel);
63                 }
64         } else {
65                 if (parallel == 0) {
66                         std::sprintf(heading, "Round %u, Machine %u", round, machine + 1);
67                 } else {
68                         std::sprintf(heading, "Round %u, Group %u, Machine %u", round, parallel, machine + 1);
69                 }
70         }
71
72         unsigned width = my_draw_text(heading, NULL, 40.0, "mainheading");
73         my_draw_text_deferred(td, heading, 40.0, "mainheading", "mainheading", LOGICAL_SCREEN_WIDTH/2 - width/2, 60);
74 }
75
76 // make column headings from the first player's songs
77 void GroupScreen::draw_column_headings(std::vector<TextDefer> &td, const Group &group, const std::vector<unsigned> &colwidth)
78 {
79         unsigned num_scores = group.players[0].scores.size();
80
81         unsigned col = 1;
82         unsigned x = 40 + colwidth[0];
83         for (std::vector<Score>::const_iterator i = group.players[0].scores.begin(); i != group.players[0].scores.end(); ++i, ++col) {
84                 if (!i->chosen) {
85                         unsigned this_width = my_draw_text(i->song.short_title, NULL, 12.0, "columnheading");
86                         my_draw_text_deferred(td, i->song.short_title, 12.0, "columnheading", "columnheading", x + colwidth[col] / 2 - this_width / 2, 100);
87                 }
88                 x += colwidth[col] + 20;
89         }
90
91         if (num_scores > 1) {
92                 unsigned this_width = my_draw_text("Total", NULL, 12.0, "columnheading");
93                 my_draw_text_deferred(td, "Total", 12.0, "columnheading", "columnheading", x + colwidth[num_scores + 1] / 2 - this_width / 2, 100);
94                 x += colwidth[num_scores + 1] + 20;
95         }
96         unsigned this_width = my_draw_text("Rank", NULL, 12.0, "columnheading");
97         my_draw_text_deferred(td, "Rank", 12.0, "columnheading", "columnheading", x + colwidth[num_scores + 2] / 2 - this_width / 2, 100);
98 }       
99         
100 // show all the players and the scores
101 void GroupScreen::draw_scores(std::vector<TextDefer> &td, const Group &group, unsigned min_player, const std::vector<unsigned> &colwidth)
102 {
103         unsigned max_num_width = my_draw_text("8888", NULL, 22.0, "score");
104         unsigned num_scores = group.players[0].scores.size();
105         unsigned show_players = get_show_players(group);
106         unsigned y = (show_players <= 7) ? 140 : (140 - (show_players - 7) * 5);
107         
108         unsigned row = 0, m = 0, x;
109         for (std::vector<Player>::const_iterator i = group.players.begin(); i != group.players.end() && row < 9; ++i) {
110                 if (m++ % num_machines != machine)
111                         continue;
112                 if (m-1 < min_player)
113                         continue;
114
115                 std::string suffix = theme_suffix_from_row(row);
116
117                 my_draw_text_deferred(td, i->nick, 18.0, "rowheading" + suffix, "rowheading" + suffix, 20, y);
118
119                 x = 40 + colwidth[0];
120
121                 unsigned col = 1;
122                 for (std::vector<Score>::const_iterator j = i->scores.begin(); j != i->scores.end(); ++j, ++col) {
123                         char text[16] = "";
124                         if (j->score != -1) {
125                                 std::sprintf(text, "%u", j->score);
126                         }
127         
128                         unsigned this_width = my_draw_text(text, NULL, 22.0, "score" + suffix);
129                         if (j->chosen) {
130                                 my_draw_text_deferred(td, text, 22.0, "score" + suffix, "freshscore" + suffix, x + max_num_width - this_width, y);
131
132                                 // draw the long name if we can, otherwise use the short one
133                                 if (my_draw_text(j->song.title, NULL, 12.0, "chosensongname") > (colwidth[col] - 10 - max_num_width)) {
134                                         my_draw_text_deferred(td, j->song.short_title, 12.0, "chosensongname" + suffix, "freshchosensongname" + suffix, x + max_num_width + 10, y);
135                                 } else {
136                                         my_draw_text_deferred(td, j->song.title, 12.0, "chosensongname" + suffix, "freshchosensongname" + suffix, x + max_num_width + 10, y);
137                                 }
138                         } else {
139                                 my_draw_text_deferred(td, text, 22.0, "score" + suffix, "freshscore" + suffix, x + colwidth[col] / 2 - this_width / 2, y);
140                         }
141                         x += colwidth[col] + 20;
142                 }
143
144                 // draw total
145                 if (num_scores > 1) {
146                         char text[16];
147                         std::sprintf(text, "%u", i->total);
148                         
149                         unsigned this_width = my_draw_text(text, NULL, 22.0, "totalscore" + suffix);
150                         my_draw_text_deferred(td, text, 22.0, "totalscore" + suffix, "freshtotalscore" + suffix, x + colwidth[num_scores + 1] / 2 - this_width / 2, y);
151                         x += colwidth[num_scores + 1] + 20;
152                 }
153
154                 if (show_players > 7)
155                         y += 40 - (show_players - 7) * 4;
156                 else 
157                         y += 40;
158                 ++row;
159         }
160 }       
161
162 /*
163  * Find out how wide each column has to be. First try unlimited width (ie.
164  * long titles for everything); if that gets too long, try again with short
165  * titles for chosen songs.
166  */
167 void GroupScreen::find_column_widths(const Group &group, std::vector<unsigned> &colwidth)
168 {
169         unsigned num_scores;
170         unsigned max_num_width = my_draw_text("8888", NULL, 22.0, "score");
171         unsigned sumcolwidth;
172         
173         for (unsigned mode = 0; mode < 2; ++mode) {
174                 for (std::vector<Player>::const_iterator i = group.players.begin(); i != group.players.end(); ++i) {
175                         unsigned col = 1;
176                         
177                         if (colwidth.size() == 0)
178                                 colwidth.push_back(0);
179                         
180                         colwidth[0] = std::max(colwidth[0], my_draw_text(i->nick, NULL, 18.0, "rowheading"));
181
182                         for (std::vector<Score>::const_iterator j = i->scores.begin(); j != i->scores.end(); ++j, ++col) {
183                                 if (colwidth.size() < col+1)
184                                         colwidth.push_back(0);
185                                         
186                                 if (j->chosen) {
187                                         colwidth[col] = std::max(colwidth[col], my_draw_text((mode == 0) ? j->song.title : j->song.short_title, NULL, 12.0, "chosensongname") + 
188                                                         max_num_width + 10);
189                                 } else {                
190                                         colwidth[col] = std::max(colwidth[col], my_draw_text(j->song.short_title, NULL, 12.0, "randomsongname"));
191                                         colwidth[col] = std::max(colwidth[col], max_num_width);
192                                 }
193                         }
194                 }
195
196                 num_scores = group.players[0].scores.size();
197
198                 if (colwidth.size() < num_scores + 2) {
199                         colwidth.push_back(0);
200                         colwidth.push_back(0);
201                 }
202         
203                 if (num_scores > 1) {
204                         colwidth[num_scores + 1] = std::max(my_draw_text("Total", NULL, 12.0, "columnheading"), max_num_width);
205                 }
206                 colwidth[num_scores + 2] = my_draw_text("Rank", NULL, 12.0, "columnheading");
207
208                 // if we're at long titles and that works, don't try the short ones
209                 sumcolwidth = 0;
210                         
211                 for (unsigned i = 0; i <= num_scores + 2; ++i)
212                         sumcolwidth += colwidth[i] + 20;
213                 
214                 if (sumcolwidth < LOGICAL_SCREEN_WIDTH - 20)
215                         break;
216
217                 if (mode == 0) {
218                         colwidth.erase(colwidth.begin(), colwidth.end());
219                 }
220         }
221
222         /* 
223          * If we have space to go, distribute as much as we can to the chosen song column, so we won't have
224          * total and rank jumping around.
225          */
226         if (sumcolwidth < LOGICAL_SCREEN_WIDTH - 20) {
227                 int first_chosen_col = -1;
228                 unsigned col = 1;
229
230                 for (std::vector<Score>::const_iterator i = group.players[0].scores.begin(); i != group.players[0].scores.end(); ++i, ++col) {
231                         if (i->chosen) {
232                                 first_chosen_col = col;
233                                 break;
234                         }
235                 }
236
237                 if (first_chosen_col != -1) {
238                         colwidth[first_chosen_col] += LOGICAL_SCREEN_WIDTH - 20 - sumcolwidth;
239                 }
240         }
241 }
242
243 /* Find the first player with the fewest songs played and part of this machine. */
244 const Player *GroupScreen::get_next_player(const Group &group)
245 {
246         unsigned min_played_songs = 9999;
247         const Player *next_player = NULL;
248         unsigned m = 0;
249         for (std::vector<Player>::const_iterator i = group.players.begin(); i != group.players.end(); ++i) {
250                 unsigned this_played = 0;
251                 for (std::vector<Score>::const_iterator j = i->scores.begin(); j != i->scores.end(); ++j) {
252                         if (j->score != -1)
253                                 ++this_played;
254                 }
255
256                 if ((m++ % num_machines == machine) && this_played < min_played_songs) {
257                         min_played_songs = this_played;
258                         next_player = &(*i);
259                 }
260         }
261
262         return next_player;
263 }
264
265 /*
266  * At the bottom, for a single player, is "who's playing, what will he/she be
267  * playing, and optionally, how much to lead/win and how much to secure
268  * qualification" (the last one only in the final round). We assume playing is
269  * done in a modified zigzag; all the random songs are played first in
270  * zigzag/wrapping order (player 1 song 1, player 2 song 2, player 3 song 3,
271  * player 1 song 2, player 2 song 3, player 3 song 1, etc... assuming three
272  * songs and three players) and then all the chosen songs are played (we assume
273  * only one chosen song).
274  *
275  * The lines are as follows:
276  *
277  * <player>
278  * <song>
279  * High score: <hs> by <hsplayer> at <hsevent>
280  * Needs to lead: <leadscore>
281  * Needs to secure qualification: <qualscore>
282  * Needs to win group: <winscore>
283  */
284 void GroupScreen::draw_next_up_single(unsigned char *buf, const Group &group,
285         std::map<unsigned, unsigned> &song_scores, std::map<unsigned, unsigned> &player_scores,
286         const std::vector<unsigned> &max_score, const std::vector<unsigned> &min_score)
287 {
288         unsigned num_scores = group.players[0].scores.size();
289         
290         // Find out how many random songs there are (equal for all players).
291         unsigned num_random_songs = 0;
292         for (std::vector<Score>::const_iterator i = group.players[0].scores.begin(); i != group.players[0].scores.end(); ++i) {
293                 if (!i->chosen)
294                         ++num_random_songs;
295         }
296
297         /* 
298          * Find out which player is next, and what song he she is supposed to play. First
299          * try random songs.
300          */
301         const Player *next_player = get_next_player(group);
302         const Score *next_song = NULL;
303
304         for (unsigned i = 0; i < num_random_songs; ++i) {
305                 unsigned j = (i + next_player->position - 1) % num_random_songs;
306                 if (next_player->scores[j].score == -1) {
307                         next_song = &(next_player->scores[j]);
308                         break;
309                 }
310         }
311
312         // then all songs, if that didn't work out (slightly icky, but hey)
313         if (next_song == NULL) {
314                 for (unsigned i = 0; i < num_scores; ++i) {
315                         unsigned j = (i + next_player->position) % num_scores;
316                         if (next_player->scores[j].score == -1) {
317                                 next_song = &(next_player->scores[j]);
318                                 break;
319                         }
320                 }
321         }
322
323         if (next_song != NULL) {
324                 // find out how many songs we've played in all
325                 unsigned num_played = 0;
326                 for (unsigned i = 0; i < num_scores; ++i) {
327                         if (next_player->scores[i].score != -1) {
328                                 ++num_played;
329                         }
330                 }
331         
332                 bool last_song = (num_played == num_scores - 1);
333                         
334                 draw_next_up_player(buf, group, *next_player, *next_song, last_song, song_scores, player_scores, max_score, min_score);
335         }
336 }
337
338 /*
339  * Some tournaments allow versus play in the initial rounds to save time; this is
340  * of course for random songs only. In this case, the scheme from draw_next_up_single()
341  * is somewhat changed, as we zig-zag across pairs instead of players. (If there's a
342  * stray person left in the group, that player plays the song by him-/herself as in
343  * a usual single tournament.
344  */
345 void GroupScreen::draw_next_up_versus(unsigned char *buf, const Group &group,
346         std::map<unsigned, unsigned> &song_scores, std::map<unsigned, unsigned> &player_scores,
347         const std::vector<unsigned> &max_score, const std::vector<unsigned> &min_score)
348 {
349         // Find out how many random songs there are (equal for all players).
350         unsigned num_random_songs = 0;
351         for (std::vector<Score>::const_iterator i = group.players[0].scores.begin(); i != group.players[0].scores.end(); ++i) {
352                 if (!i->chosen)
353                         ++num_random_songs;
354         }
355
356         // Find the next player and what song he/she is supposed to play, if any.
357         const Player *next_player = get_next_player(group);
358         const Score *next_song = NULL;
359         unsigned song_num;
360
361         for (unsigned i = 0; i < num_random_songs; ++i) {
362                 unsigned j = (i + (next_player->position - 1) / 2) % num_random_songs;
363                 if (next_player->scores[j].score == -1) {
364                         next_song = &(next_player->scores[j]);
365                         song_num = j;
366                         break;
367                 }
368         }
369         
370         /*
371          * If there's no match, we're on the chosen songs (or done),
372          * so just delegate to draw_up_single().
373          */
374         if (next_song == NULL) {
375                 draw_next_up_single(buf, group, song_scores, player_scores, max_score, min_score);
376                 return;
377         }
378         
379         /*
380          * Look for a player with the same amount of random songs played _and_ missing
381          * the same song.
382          */ 
383         unsigned num_songs_played = 0;
384         for (unsigned i = 0; i < num_random_songs; ++i) {
385                 if (next_player->scores[i].score != -1) {
386                         ++num_songs_played;
387                 }
388         }
389         
390         unsigned m = 0;
391         const Player *other_player = NULL;
392         for (std::vector<Player>::const_iterator i = group.players.begin(); i != group.players.end(); ++i) {
393                 if ((m++ % num_machines != machine))
394                         continue;
395                 if (i->id == next_player->id)
396                         continue;
397                 
398                 unsigned this_songs_played = 0;
399                 for (unsigned j = 0; j < num_random_songs; ++j) {
400                         if (i->scores[j].score != -1) {
401                                 ++this_songs_played;
402                         }
403                 }
404
405                 if (this_songs_played != num_songs_played)
406                         continue;
407
408                 if (i->scores[song_num].score == -1) {
409                         other_player = &(*i);
410                         break;
411                 }       
412         }
413
414         // If we didn't find another player, just draw the one we have as usual.
415         if (other_player == NULL) {
416                 draw_next_up_player(buf, group, *next_player, *next_song, false,
417                         song_scores, player_scores, max_score, min_score);
418                 return;
419         }
420         
421         // OK, we have two players. Draw their nicks and the scores
422         widestring text = widestring("Next players: ") + next_player->nick + widestring(" and ") + other_player->nick;
423         unsigned this_width = my_draw_text(text, NULL, 24.0, "nextsonginfo.player");
424         my_draw_text(text, buf, 24.0, "nextsonginfo.player", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, 420);
425
426         if (next_song->song.id != -1) {
427                 this_width = my_draw_text(next_song->song.title, NULL, 20.0, "nextsonginfo.title");
428                 my_draw_text(next_song->song.title, buf, 20.0, "nextsonginfo.title", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, 457);
429
430                 Highscore hs;
431                 conn.perform(FetchHighscore(next_song->song.id, &hs));
432
433                 if (hs.score != -1) {
434                         text = widestring("High score: ") + widestring(pqxx::to_string(hs.score)) +
435                                 widestring(", by ") + hs.nick + widestring(" in ") + hs.tournament_name;
436                         this_width = my_draw_text(text, NULL, 16.0, "nextsonginfo.highscore");
437                         my_draw_text(text, buf, 16.0, "nextsonginfo.highscore", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, 487);
438                 }
439         }
440 }
441
442 void GroupScreen::draw_next_up_player(unsigned char *buf, const Group &group, const Player &player, const Score &song, bool last_song,
443         std::map<unsigned, unsigned> &song_scores, std::map<unsigned, unsigned> &player_scores,
444         const std::vector<unsigned> &max_score, const std::vector<unsigned> &min_score)
445 {
446         widestring text = widestring("Next player: ") + player.nick;
447         unsigned this_width = my_draw_text(text, NULL, 24.0, "nextsonginfo.player");
448         my_draw_text(text, buf, 24.0, "nextsonginfo.player", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, 420);
449
450         if (song.song.id != -1) {
451                 this_width = my_draw_text(song.song.title, NULL, 20.0, "nextsonginfo.title");
452                 my_draw_text(song.song.title, buf, 20.0, "nextsonginfo.title", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, 457);
453
454                 Highscore hs;
455                 conn.perform(FetchHighscore(song.song.id, &hs));
456
457                 if (hs.score != -1) {
458                         text = widestring("High score: ") + widestring(pqxx::to_string(hs.score)) +
459                                 widestring(", by ") + hs.nick + widestring(" in ") + hs.tournament_name;
460                         this_width = my_draw_text(text, NULL, 16.0, "nextsonginfo.highscore");
461                         my_draw_text(text, buf, 16.0, "nextsonginfo.highscore", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, 487);
462                 }
463         }
464
465         // only show lead/win/qualify for the last song
466         if (last_song) {
467                 /*
468                  * Find out how much we need to lead, how much we need to be guaranteed
469                  * to win the group, and how much we need to secure qualification.
470                  */
471
472                 // find the best score we can get
473                 unsigned max_score_this_song;
474                 if (song.song.id != -1) {
475                         // random song, or we know what song the player picked
476                         max_score_this_song = song_scores[song.song.id];
477                 } else {
478                         max_score_this_song = player_scores[player.id];
479                 }
480
481                 unsigned y = 520;
482
483                 // see what score this player must beat to lead
484                 unsigned lead_beat = 0, win_beat = 0;
485                 for (unsigned i = 0; i < group.players.size(); ++i) {
486                         if (group.players[i].id == player.id)
487                                 continue;
488
489                         lead_beat = std::max(lead_beat, group.players[i].total);
490                 }
491
492                 // find the best max score among the others
493                 for (unsigned i = 0; i < group.players.size(); ++i) {
494                         if (group.players[i].id == player.id)
495                                 continue;
496
497                         win_beat = std::max(win_beat, max_score[i]);
498                 }
499
500                 /*
501                  * There's a somewhat subtle point here. Normally, what a player would be interested in
502                  * with regard to qualification would be a set of three values:
503                  *
504                  * 1. How much is the absolute minimum required to qualify, given that all others
505                  *    fail?
506                  * 2. How much will give a reasonable chance of qualifying, given the expected performance
507                  *    of all the others?
508                  * 3. How much will be enough to secure qualification, no matter what?
509                  *
510                  * Given perfect guessing, #2 would be "how much is needed to qualify"; however, it is
511                  * completely impossible to give an exact value for that, and we're not into the guessing
512                  * games. :-) #1 is often so low it's completely unrealistic (ie. far enough from #2 that
513                  * it's not interesting), but #3, the most conservative estimate, is often a good measure.
514                  * #3 is "how much is needed to _secure_ qualification", and that is usually what we
515                  * print out when it's possible.
516                  *
517                  * However, in a few situations, #1 and #3 will be the exact same value, from which it
518                  * follows (from the squeeze law, or just common sense :-) ) that #2 will be the same
519                  * value as #1 and #3. (This usually happens near or at the end of a group.) In that
520                  * case, we know the value we seek (ie. "how much is needed to qualify"), so we drop
521                  * the word "secure" and just print it as-is.
522                  *
523                  * To find #1 and #3, we sort and pick out the values we need to beat in the best and
524                  * the worst case.
525                  */
526                 int qualify_beat_worst_case = -1, qualify_beat_best_case = -1;
527
528                 if (group.num_qualifying > 0) {
529                         std::vector<unsigned> tmp;
530
531                         for (unsigned i = 0; i < group.players.size(); ++i) {
532                                 if (group.players[i].id == player.id)
533                                         continue;
534                                 tmp.push_back(max_score[i]);
535                         }
536                         std::sort(tmp.begin(), tmp.end());
537                         if (tmp.size() >= group.num_qualifying) {
538                                 qualify_beat_worst_case = tmp[tmp.size() - group.num_qualifying];
539                         } else {
540                                 qualify_beat_worst_case = 0;
541                         }
542
543                         std::vector<unsigned> tmp2;
544                         for (unsigned i = 0; i < group.players.size(); ++i) {
545                                 if (group.players[i].id == player.id)
546                                         continue;
547                                 tmp2.push_back(min_score[i]);
548                         }
549
550                         std::sort(tmp2.begin(), tmp2.end());
551                         if (tmp2.size() >= group.num_qualifying) {
552                                 qualify_beat_best_case = tmp2[tmp2.size() - group.num_qualifying];
553                         } else {
554                                 qualify_beat_best_case = 0;
555                         }
556                 }
557
558                 // print out the lines we can attain
559                 if (player.total + max_score_this_song > lead_beat && (lead_beat != win_beat)) {
560                         int lead_need = std::max(lead_beat - player.total + 1, 0U);
561
562                         if (lead_need > 1) {
563                                 text = widestring("Needs to lead: ") + widestring(pqxx::to_string(lead_need));
564                                 this_width = my_draw_text(text, NULL, 18.0, "need");
565                                 my_draw_text(text, buf, 18.0, "need", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, y);
566
567                                 y += 30;
568                         }
569                 }
570
571                 if (player.total + max_score_this_song > win_beat) {
572                         int win_need = std::max(win_beat - player.total + 1, 0U);
573
574                         if (win_need > 0) {
575                                 text = widestring("Needs to win: ") + widestring(pqxx::to_string(win_need));
576
577                                 this_width = my_draw_text(text, NULL, 18.0, "need");
578                                 my_draw_text(text, buf, 18.0, "need", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, y);
579
580                                 y += 30;
581                         }
582                 }
583
584                 if (group.num_qualifying > 0 &&
585                     group.num_qualifying != group.players.size() &&
586                                 player.total + max_score_this_song > unsigned(qualify_beat_worst_case) &&
587                                 (unsigned(qualify_beat_worst_case) != win_beat)) {
588                         int qual_need = std::max(qualify_beat_worst_case - player.total + 1, 0U);
589
590                         if (qual_need > 0) {
591                                 if (qualify_beat_worst_case == qualify_beat_best_case) {
592                                         text = widestring("Needs to qualify: ") + widestring(pqxx::to_string(qual_need));
593                                 } else {
594                                         text = widestring("Needs to secure qualification: ") + widestring(pqxx::to_string(qual_need));
595                                 }
596
597                                 this_width = my_draw_text(text, NULL, 18.0, "need");
598                                 my_draw_text(text, buf, 18.0, "need", (LOGICAL_SCREEN_WIDTH/2) - this_width/2, y);
599
600                                 y += 30;
601                         }
602                 }
603         }
604 }
605
606 // some refactoring done, more should be
607 void GroupScreen::draw(unsigned char *buf, unsigned width, unsigned height)
608 {
609         std::vector<TextDefer> td;
610         
611         scores_changed.reset_flag();
612         set_screen_size(width, height);
613
614         /*
615          * We'll probably need some values from here later on (although not all), just fetch them
616          * all while we're at it.
617          */
618         std::map<unsigned, unsigned> song_scores, player_scores;
619         conn.perform(FetchMaxScoreForSongs(tournament, &song_scores));
620         conn.perform(FetchMaxScoreForPlayers(tournament, round, &player_scores));
621         
622         Group group;
623         conn.perform(FetchGroup(tournament, round, parallel, &group));
624         gettimeofday(&last_updated, NULL);
625
626         fill_background(buf, "groupscreen", width, height);
627
628         std::vector<unsigned> colwidth;
629         
630         draw_main_heading(td);
631         find_column_widths(group, colwidth);
632         draw_column_headings(td, group, colwidth);
633
634         // Find out which player is next. we want to show SHOW_PLAYERS players, centered
635         // around this as much as possible. (Usually, this will mean all, but not always.)
636         unsigned show_players = get_show_players(group);
637         const Player *center_player = get_next_player(group);
638         
639         // find the index (kind of backwards...)
640         int player_index = -1;
641         for (unsigned i = 0; i < group.players.size(); ++i) {
642                 if (&(group.players[i]) == center_player) {
643                         player_index = i;
644                         break;
645                 }
646         }
647
648         assert(player_index >= 0);
649
650         int min_player = player_index - signed(show_players) / 2;
651         if (min_player + show_players > group.players.size()) // FIXME: songs_per_machine
652                 min_player = group.players.size() - show_players;
653         if (min_player < 0)
654                 min_player = 0;
655
656         draw_scores(td, group, min_player, colwidth);
657         
658         unsigned num_scores = group.players[0].scores.size();
659
660         /*
661          * Approximate (but probably working quite well in practice) heuristic
662          * for finding the min and max rank of a player works as follows:
663          *
664          * First of all, find out, for each player in the group, what the
665          * maximum remaining score possibly can be (the minimum score is of
666          * course identical to the player's current total). For a random song,
667          * this is of course 1000 * (maximum feet rating) (but of course, that
668          * depends on whether we can play single or double! for now, assume
669          * double is okay, but this logic will be deferred to FetchMaxScore
670          * anyhow); for a random song, we simply pick the highest-ranking song
671          * we can find, EXCEPT those the player has chosen earlier AND the
672          * random songs this round, AND all random songs from elimination rounds
673          * (ie. rounds with only one group). (Phew!) This doesn't solve problems
674          * we'd face with more than one chosen song, but it should be good enough.
675          *
676          * After we've found the max and min scores for all players, it's a simple
677          * matter of sorting; the best attainable rank for player X is obtained if 
678          * X gets max score and all others get min score, the worst attainable rank
679          * is obtained if X gets min score and all others get max score.
680          */
681         std::vector<unsigned> max_score, min_score;
682         for (std::vector<Player>::const_iterator i = group.players.begin(); i != group.players.end(); ++i) {
683                 unsigned min_score_tp = 0, max_score_tp = 0;
684                 for (std::vector<Score>::const_iterator j = i->scores.begin(); j != i->scores.end(); ++j) {
685                         if (j->score != -1) {
686                                 // already given
687                                 min_score_tp += j->score;
688                                 max_score_tp += j->score;
689                         } else {
690                                 unsigned max_score_this_song;
691                                 if (j->song.id != -1) {
692                                         // random song, or we know what song the player picked
693                                         max_score_this_song = song_scores[j->song.id];
694                                 } else {
695                                         max_score_this_song = player_scores[i->id];
696                                 }
697                                 max_score_tp += max_score_this_song;
698                         }
699                 }
700                 max_score.push_back(max_score_tp);
701                 min_score.push_back(min_score_tp);
702         }
703         
704         // now finally find min and max rank, and draw it all
705         unsigned y = (show_players <= 7) ? 140 : (140 - (show_players - 7) * 5);
706         unsigned row = 0;
707         for (unsigned i = 0; i < group.players.size() && (i/num_machines) < show_players+min_player; ++i) {
708                 unsigned best_rank = 1, worst_rank = 1;
709                 for (unsigned j = 0; j < group.players.size(); ++j) {
710                         if (i == j)
711                                 continue;
712
713                         if (max_score[i] < min_score[j])
714                                 ++best_rank;
715                         if (min_score[i] <= max_score[j])
716                                 ++worst_rank;
717                 }
718
719                 char text[16];
720                 if (best_rank == worst_rank)
721                         std::sprintf(text, "%u", best_rank);
722                 else
723                         std::sprintf(text, "%u-%u", best_rank, worst_rank);
724                 
725                 if (i % num_machines != machine)
726                         continue;
727                 if (signed(i) < min_player)
728                         continue;
729                 
730                 std::string suffix = theme_suffix_from_row(row);
731
732                 // find out where to place this
733                 unsigned x = 40 + colwidth[0];
734                 for (unsigned j = 1; j <= num_scores + 1; ++j)
735                         x += colwidth[j] + 20;
736
737                 // minor correction :-)
738                 if (num_scores <= 1)
739                         x -= 20;
740                 
741                 unsigned this_width = my_draw_text(text, NULL, 22.0, "rank" + suffix);
742                 my_draw_text_deferred(td, text, 22.0, "rank" + suffix, "freshrank" + suffix, x + colwidth[num_scores + 2] / 2 - this_width / 2, y);
743
744                 if (show_players > 7)
745                         y += 40 - (show_players - 7) * 4;
746                 else 
747                         y += 40;
748
749                 ++row;
750         }
751
752         if (players_per_machine == 2)
753                 draw_next_up_versus(buf, group, song_scores, player_scores, max_score, min_score);
754         else 
755                 draw_next_up_single(buf, group, song_scores, player_scores, max_score, min_score);
756         
757         valid = true;
758         draw_all_deferred_text(buf, td, last_text);
759         last_text = td;
760 }
761
762 int GroupScreen::get_priority()
763 {
764         return 10;
765 }