Blob


1 /* $NetBSD: scores.c,v 1.26 2021/05/02 12:50:46 rillig Exp $ */
3 /*-
4 * Copyright (c) 1992, 1993
5 * The Regents of the University of California. All rights reserved.
6 *
7 * This code is derived from software contributed to Berkeley by
8 * Chris Torek and Darren F. Provine.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
12 * are met:
13 * 1. Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in the
17 * documentation and/or other materials provided with the distribution.
18 * 3. Neither the name of the University nor the names of its contributors
19 * may be used to endorse or promote products derived from this software
20 * without specific prior written permission.
21 *
22 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
23 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
26 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
28 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
29 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
31 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32 * SUCH DAMAGE.
33 *
34 * @(#)scores.c 8.1 (Berkeley) 5/31/93
35 */
37 /*
38 * Score code for Tetris, by Darren Provine (kilroy@gboro.glassboro.edu)
39 * modified 22 January 1992, to limit the number of entries any one
40 * person has.
41 *
42 * Major whacks since then.
43 */
44 #include <err.h>
45 #include <errno.h>
46 #include <fcntl.h>
47 #include <pwd.h>
48 #include <stdio.h>
49 #include <stdlib.h>
50 #include <string.h>
51 #include <sys/stat.h>
52 #include <time.h>
53 #include <term.h>
54 #include <unistd.h>
56 #ifdef __FreeBSD__
57 # include <sys/endian.h>
58 # define swap32 bswap32
59 # define swap64 bswap64
60 #elif defined(__linux__)
61 # include <sys/file.h>
62 # define swap32 __builtin_bswap32
63 # define swap64 __builtin_bswap64
64 #endif
66 #include "pathnames.h"
67 #include "screen.h"
68 #include "scores.h"
69 #include "tetris.h"
71 /*
72 * Allow updating the high scores unless we're built as part of /rescue.
73 */
74 #ifndef RESCUEDIR
75 #define ALLOW_SCORE_UPDATES
76 #endif
78 /*
79 * Within this code, we can hang onto one extra "high score", leaving
80 * room for our current score (whether or not it is high).
81 *
82 * We also sometimes keep tabs on the "highest" score on each level.
83 * As long as the scores are kept sorted, this is simply the first one at
84 * that level.
85 */
86 #define NUMSPOTS (MAXHISCORES + 1)
87 #define NLEVELS (MAXLEVEL + 1)
89 static time_t now;
90 static int nscores;
91 static int gotscores;
92 static struct highscore scores[NUMSPOTS];
94 static int checkscores(struct highscore *, int);
95 static int cmpscores(const void *, const void *);
96 static void getscores(int *);
97 static void printem(int, int, struct highscore *, int, const char *);
98 static char *thisuser(void);
100 /* contents chosen to be a highly illegal username */
101 static const char hsh_magic_val[HSH_MAGIC_SIZE] = "//:\0\0://";
103 #define HSH_ENDIAN_NATIVE 0x12345678
104 #define HSH_ENDIAN_OPP 0x78563412
106 /* current file format version */
107 #define HSH_VERSION 1
109 /* codes for scorefile_probe return */
110 #define SCOREFILE_ERROR (-1)
111 #define SCOREFILE_CURRENT 0 /* 40-byte */
112 #define SCOREFILE_CURRENT_OPP 1 /* 40-byte, opposite-endian */
113 #define SCOREFILE_599 2 /* 36-byte */
114 #define SCOREFILE_599_OPP 3 /* 36-byte, opposite-endian */
115 #define SCOREFILE_50 4 /* 32-byte */
116 #define SCOREFILE_50_OPP 5 /* 32-byte, opposite-endian */
118 /*
119 * Check (or guess) what kind of score file contents we have.
120 */
121 static int
122 scorefile_probe(int sd)
124 struct stat st;
125 int t1, t2, t3, tx;
126 ssize_t result;
127 uint32_t numbers[3], offset56, offset60, offset64;
129 if (fstat(sd, &st) < 0) {
130 warn("Score file %s: fstat", _PATH_SCOREFILE);
131 return -1;
134 t1 = st.st_size % sizeof(struct highscore_ondisk) == 0;
135 t2 = st.st_size % sizeof(struct highscore_ondisk_599) == 0;
136 t3 = st.st_size % sizeof(struct highscore_ondisk_50) == 0;
137 tx = t1 + t2 + t3;
138 if (tx == 1) {
139 /* Size matches exact number of one kind of records */
140 if (t1) {
141 return SCOREFILE_CURRENT;
142 } else if (t2) {
143 return SCOREFILE_599;
144 } else {
145 return SCOREFILE_50;
147 } else if (tx == 0) {
148 /* Size matches nothing, pick most likely as default */
149 goto wildguess;
152 /*
153 * File size is multiple of more than one structure size.
154 * (For example, 288 bytes could be 9*hso50 or 8*hso599.)
155 * Read the file and see if we can figure out what's going
156 * on. This is the layout of the first two records:
158 * offset hso / current hso_599 hso_50
159 * (40-byte) (36-byte) (32-byte)
161 * 0 name #0 name #0 name #0
162 * 4 : : :
163 * 8 : : :
164 * 12 : : :
165 * 16 : : :
166 * 20 score #0 score #0 score #0
167 * 24 level #0 level #0 level #0
168 * 28 (pad) time #0 time #0
169 * 32 time #0 name #1
170 * 36 name #1 :
171 * 40 name #1 : :
172 * 44 : : :
173 * 48 : : :
174 * 52 : : score #1
175 * 56 : score #1 level #1
176 * 60 score #1 level #1 time #1
177 * 64 level #1 time #1 name #2
178 * 68 (pad) : :
179 * 72 time #1 name #2 :
180 * 76 : : :
181 * 80 --- end ---
183 * There are a number of things we could check here, but the
184 * most effective test is based on the following restrictions:
186 * - The level must be between 1 and 9 (inclusive)
187 * - All times must be after 1985 and are before 2038,
188 * so the high word must be 0 and the low word may not be
189 * a small value.
190 * - Integer values of 0 or 1-9 cannot be the beginning of
191 * a login name string.
192 * - Values of 1-9 are probably not a score.
194 * So we read the three words at offsets 56, 60, and 64, and
195 * poke at the values to try to figure things...
196 */
198 if (lseek(sd, 56, SEEK_SET) < 0) {
199 warn("Score file %s: lseek", _PATH_SCOREFILE);
200 return -1;
202 result = read(sd, &numbers, sizeof(numbers));
203 if (result < 0) {
204 warn("Score file %s: read", _PATH_SCOREFILE);
205 return -1;
207 if ((size_t)result != sizeof(numbers)) {
208 /*
209 * The smallest file whose size divides by more than
210 * one of the sizes is substantially larger than 64,
211 * so this should *never* happen.
212 */
213 warnx("Score file %s: Unexpected EOF", _PATH_SCOREFILE);
214 return -1;
217 offset56 = numbers[0];
218 offset60 = numbers[1];
219 offset64 = numbers[2];
221 if (offset64 >= MINLEVEL && offset64 <= MAXLEVEL) {
222 /* 40-byte structure */
223 return SCOREFILE_CURRENT;
224 } else if (offset60 >= MINLEVEL && offset60 <= MAXLEVEL) {
225 /* 36-byte structure */
226 return SCOREFILE_599;
227 } else if (offset56 >= MINLEVEL && offset56 <= MAXLEVEL) {
228 /* 32-byte structure */
229 return SCOREFILE_50;
232 /* None was a valid level; try opposite endian */
233 offset64 = swap32(offset64);
234 offset60 = swap32(offset60);
235 offset56 = swap32(offset56);
237 if (offset64 >= MINLEVEL && offset64 <= MAXLEVEL) {
238 /* 40-byte structure */
239 return SCOREFILE_CURRENT_OPP;
240 } else if (offset60 >= MINLEVEL && offset60 <= MAXLEVEL) {
241 /* 36-byte structure */
242 return SCOREFILE_599_OPP;
243 } else if (offset56 >= MINLEVEL && offset56 <= MAXLEVEL) {
244 /* 32-byte structure */
245 return SCOREFILE_50_OPP;
248 /* That didn't work either, dunno what's going on */
249 wildguess:
250 warnx("Score file %s is likely corrupt", _PATH_SCOREFILE);
251 if (sizeof(void *) == 8 && sizeof(time_t) == 8) {
252 return SCOREFILE_CURRENT;
253 } else if (sizeof(time_t) == 8) {
254 return SCOREFILE_599;
255 } else {
256 return SCOREFILE_50;
260 /*
261 * Copy a string safely, making sure it's null-terminated.
262 */
263 static void
264 readname(char *to, size_t maxto, const char *from, size_t maxfrom)
266 size_t amt;
268 amt = maxto < maxfrom ? maxto : maxfrom;
269 memcpy(to, from, amt);
270 to[maxto-1] = '\0';
273 /*
274 * Copy integers, byte-swapping if desired.
275 */
276 static int32_t
277 read32(int32_t val, int doflip)
279 if (doflip) {
280 val = swap32(val);
282 return val;
285 static int64_t
286 read64(int64_t val, int doflip)
288 if (doflip) {
289 val = swap64(val);
291 return val;
294 /*
295 * Read up to MAXHISCORES scorefile_ondisk entries.
296 */
297 static int
298 readscores(int sd, int doflip)
300 struct highscore_ondisk buf[MAXHISCORES];
301 ssize_t result;
302 int i;
304 result = read(sd, buf, sizeof(buf));
305 if (result < 0) {
306 warn("Score file %s: read", _PATH_SCOREFILE);
307 return -1;
309 nscores = result / sizeof(buf[0]);
311 for (i=0; i<nscores; i++) {
312 readname(scores[i].hs_name, sizeof(scores[i].hs_name),
313 buf[i].hso_name, sizeof(buf[i].hso_name));
314 scores[i].hs_score = read32(buf[i].hso_score, doflip);
315 scores[i].hs_level = read32(buf[i].hso_level, doflip);
316 scores[i].hs_time = read64(buf[i].hso_time, doflip);
318 return 0;
321 /*
322 * Read up to MAXHISCORES scorefile_ondisk_599 entries.
323 */
324 static int
325 readscores599(int sd, int doflip)
327 struct highscore_ondisk_599 buf[MAXHISCORES];
328 ssize_t result;
329 int i;
331 result = read(sd, buf, sizeof(buf));
332 if (result < 0) {
333 warn("Score file %s: read", _PATH_SCOREFILE);
334 return -1;
336 nscores = result / sizeof(buf[0]);
338 for (i=0; i<nscores; i++) {
339 readname(scores[i].hs_name, sizeof(scores[i].hs_name),
340 buf[i].hso599_name, sizeof(buf[i].hso599_name));
341 scores[i].hs_score = read32(buf[i].hso599_score, doflip);
342 scores[i].hs_level = read32(buf[i].hso599_level, doflip);
343 /*
344 * Don't bother pasting the time together into a
345 * 64-bit value; just take whichever half is nonzero.
346 */
347 scores[i].hs_time =
348 read32(buf[i].hso599_time[buf[i].hso599_time[0] == 0],
349 doflip);
351 return 0;
354 /*
355 * Read up to MAXHISCORES scorefile_ondisk_50 entries.
356 */
357 static int
358 readscores50(int sd, int doflip)
360 struct highscore_ondisk_50 buf[MAXHISCORES];
361 ssize_t result;
362 int i;
364 result = read(sd, buf, sizeof(buf));
365 if (result < 0) {
366 warn("Score file %s: read", _PATH_SCOREFILE);
367 return -1;
369 nscores = result / sizeof(buf[0]);
371 for (i=0; i<nscores; i++) {
372 readname(scores[i].hs_name, sizeof(scores[i].hs_name),
373 buf[i].hso50_name, sizeof(buf[i].hso50_name));
374 scores[i].hs_score = read32(buf[i].hso50_score, doflip);
375 scores[i].hs_level = read32(buf[i].hso50_level, doflip);
376 scores[i].hs_time = read32(buf[i].hso50_time, doflip);
378 return 0;
381 /*
382 * Read the score file. Can be called from savescore (before showscores)
383 * or showscores (if savescore will not be called). If the given pointer
384 * is not NULL, sets *fdp to an open file handle that corresponds to a
385 * read/write score file that is locked with LOCK_EX. Otherwise, the
386 * file is locked with LOCK_SH for the read and closed before return.
387 */
388 static void
389 getscores(int *fdp)
391 struct highscore_header header;
392 int sd, mint, lck;
393 mode_t mask;
394 const char *human;
395 int doflip;
396 int serrno;
397 ssize_t result;
399 #ifdef ALLOW_SCORE_UPDATES
400 if (fdp != NULL) {
401 mint = O_RDWR | O_CREAT;
402 human = "read/write";
403 lck = LOCK_EX;
404 } else
405 #endif
407 mint = O_RDONLY;
408 human = "reading";
409 lck = LOCK_SH;
411 setegid(egid);
412 mask = umask(S_IWOTH);
413 sd = open(_PATH_SCOREFILE, mint, 0666);
414 serrno = errno;
415 (void)umask(mask);
416 setegid(gid);
417 if (sd < 0) {
418 /*
419 * If the file simply isn't there because nobody's
420 * played yet, and we aren't going to be trying to
421 * update it, don't warn. Even if we are going to be
422 * trying to write it, don't fail -- we can still show
423 * the player the score they got.
424 */
425 errno = serrno;
426 if (fdp != NULL || errno != ENOENT) {
427 warn("Cannot open %s for %s", _PATH_SCOREFILE, human);
429 goto fail;
432 /*
433 * Grab a lock.
434 * XXX: failure here should probably be more fatal than this.
435 */
436 if (flock(sd, lck))
437 warn("warning: score file %s cannot be locked",
438 _PATH_SCOREFILE);
440 /*
441 * The current format (since -current of 20090525) is
443 * struct highscore_header
444 * up to MAXHIGHSCORES x struct highscore_ondisk
446 * Before this, there is no header, and the contents
447 * might be any of three formats:
449 * highscore_ondisk (64-bit machines with 64-bit time_t)
450 * highscore_ondisk_599 (32-bit machines with 64-bit time_t)
451 * highscore_ondisk_50 (32-bit machines with 32-bit time_t)
453 * The first two appear in 5.99 between the time_t change and
454 * 20090525, depending on whether the compiler inserts
455 * structure padding before an unaligned 64-bit time_t. The
456 * last appears in 5.0 and earlier.
458 * Any or all of these might also appear on other OSes where
459 * this code has been ported.
461 * Since the old file has no header, we will have to guess
462 * which of these formats it has.
463 */
465 /*
466 * First, look for a header.
467 */
468 result = read(sd, &header, sizeof(header));
469 if (result < 0) {
470 warn("Score file %s: read", _PATH_SCOREFILE);
471 goto sdfail;
473 if (result != 0 && (size_t)result != sizeof(header)) {
474 warnx("Score file %s: read: unexpected EOF", _PATH_SCOREFILE);
475 /*
476 * File is hopelessly corrupt, might as well truncate it
477 * and start over with empty scores.
478 */
479 if (lseek(sd, 0, SEEK_SET) < 0) {
480 /* ? */
481 warn("Score file %s: lseek", _PATH_SCOREFILE);
482 goto sdfail;
484 if (ftruncate(sd, 0) == 0) {
485 result = 0;
486 } else {
487 goto sdfail;
491 if (result == 0) {
492 /* Empty file; that just means there are no scores. */
493 nscores = 0;
494 } else {
495 /*
496 * Is what we read a header, or the first 16 bytes of
497 * a score entry? hsh_magic_val is chosen to be
498 * something that is extremely unlikely to appear in
499 * hs_name[].
500 */
501 if (!memcmp(header.hsh_magic, hsh_magic_val, HSH_MAGIC_SIZE)) {
502 /* Yes, we have a header. */
504 if (header.hsh_endiantag == HSH_ENDIAN_NATIVE) {
505 /* native endian */
506 doflip = 0;
507 } else if (header.hsh_endiantag == HSH_ENDIAN_OPP) {
508 doflip = 1;
509 } else {
510 warnx("Score file %s: Unknown endian tag %u",
511 _PATH_SCOREFILE, header.hsh_endiantag);
512 goto sdfail;
515 if (header.hsh_version != HSH_VERSION) {
516 warnx("Score file %s: Unknown version code %u",
517 _PATH_SCOREFILE, header.hsh_version);
518 goto sdfail;
521 if (readscores(sd, doflip) < 0) {
522 goto sdfail;
524 } else {
525 /*
526 * Ok, it wasn't a header. Try to figure out what
527 * size records we have.
528 */
529 result = scorefile_probe(sd);
530 if (lseek(sd, 0, SEEK_SET) < 0) {
531 warn("Score file %s: lseek", _PATH_SCOREFILE);
532 goto sdfail;
534 switch (result) {
535 case SCOREFILE_CURRENT:
536 result = readscores(sd, 0 /* don't flip */);
537 break;
538 case SCOREFILE_CURRENT_OPP:
539 result = readscores(sd, 1 /* do flip */);
540 break;
541 case SCOREFILE_599:
542 result = readscores599(sd, 0 /* don't flip */);
543 break;
544 case SCOREFILE_599_OPP:
545 result = readscores599(sd, 1 /* do flip */);
546 break;
547 case SCOREFILE_50:
548 result = readscores50(sd, 0 /* don't flip */);
549 break;
550 case SCOREFILE_50_OPP:
551 result = readscores50(sd, 1 /* do flip */);
552 break;
553 default:
554 goto sdfail;
556 if (result < 0) {
557 goto sdfail;
563 if (fdp)
564 *fdp = sd;
565 else
566 close(sd);
568 return;
570 sdfail:
571 close(sd);
572 fail:
573 if (fdp != NULL) {
574 *fdp = -1;
576 nscores = 0;
579 #ifdef ALLOW_SCORE_UPDATES
580 /*
581 * Paranoid write wrapper; unlike fwrite() it preserves errno.
582 */
583 static int
584 dowrite(int sd, const void *vbuf, size_t len)
586 const char *buf = vbuf;
587 ssize_t result;
588 size_t done = 0;
590 while (done < len) {
591 result = write(sd, buf+done, len-done);
592 if (result < 0) {
593 if (errno == EINTR) {
594 continue;
596 return -1;
598 done += result;
600 return 0;
602 #endif /* ALLOW_SCORE_UPDATES */
604 /*
605 * Write the score file out.
606 */
607 static void
608 putscores(int sd)
610 #ifdef ALLOW_SCORE_UPDATES
611 struct highscore_header header;
612 struct highscore_ondisk buf[MAXHISCORES] = {0};
613 int i;
615 if (sd == -1) {
616 return;
619 memcpy(header.hsh_magic, hsh_magic_val, HSH_MAGIC_SIZE);
620 header.hsh_endiantag = HSH_ENDIAN_NATIVE;
621 header.hsh_version = HSH_VERSION;
623 for (i=0; i<nscores; i++) {
624 memcpy(buf[i].hso_name, scores[i].hs_name,
625 sizeof(buf[i].hso_name));
626 buf[i].hso_score = scores[i].hs_score;
627 buf[i].hso_level = scores[i].hs_level;
628 buf[i].hso_pad = 0xbaadf00d;
629 buf[i].hso_time = scores[i].hs_time;
632 if (lseek(sd, 0, SEEK_SET) < 0) {
633 warn("Score file %s: lseek", _PATH_SCOREFILE);
634 goto fail;
636 if (dowrite(sd, &header, sizeof(header)) < 0 ||
637 dowrite(sd, buf, sizeof(buf[0]) * nscores) < 0) {
638 warn("Score file %s: write", _PATH_SCOREFILE);
639 goto fail;
641 return;
642 fail:
643 warnx("high scores may be damaged");
644 #else
645 (void)sd;
646 #endif /* ALLOW_SCORE_UPDATES */
649 /*
650 * Close the score file.
651 */
652 static void
653 closescores(int sd)
655 flock(sd, LOCK_UN);
656 close(sd);
659 /*
660 * Read and update the scores file with the current reults.
661 */
662 void
663 savescore(int level)
665 struct highscore *sp;
666 int i;
667 int change;
668 int sd;
669 const char *me;
671 getscores(&sd);
672 gotscores = 1;
673 (void)time(&now);
675 /*
676 * Allow at most one score per person per level -- see if we
677 * can replace an existing score, or (easiest) do nothing.
678 * Otherwise add new score at end (there is always room).
679 */
680 change = 0;
681 me = thisuser();
682 for (i = 0, sp = &scores[0]; i < nscores; i++, sp++) {
683 if (sp->hs_level != level || strcmp(sp->hs_name, me) != 0)
684 continue;
685 if (score > sp->hs_score) {
686 (void)printf("%s bettered %s %d score of %d!\n",
687 "\nYou", "your old level", level,
688 sp->hs_score * sp->hs_level);
689 sp->hs_score = score; /* new score */
690 sp->hs_time = now; /* and time */
691 change = 1;
692 } else if (score == sp->hs_score) {
693 (void)printf("%s tied %s %d high score.\n",
694 "\nYou", "your old level", level);
695 sp->hs_time = now; /* renew it */
696 change = 1; /* gotta rewrite, sigh */
697 } /* else new score < old score: do nothing */
698 break;
700 if (i >= nscores) {
701 strcpy(sp->hs_name, me);
702 sp->hs_level = level;
703 sp->hs_score = score;
704 sp->hs_time = now;
705 nscores++;
706 change = 1;
709 if (change) {
710 /*
711 * Sort & clean the scores, then rewrite.
712 */
713 nscores = checkscores(scores, nscores);
714 putscores(sd);
716 closescores(sd);
719 /*
720 * Get login name, or if that fails, get something suitable.
721 * The result is always trimmed to fit in a score.
722 */
723 static char *
724 thisuser(void)
726 const char *p;
727 struct passwd *pw;
728 size_t l;
729 static char u[sizeof(scores[0].hs_name)];
731 if (u[0])
732 return (u);
733 p = getlogin();
734 if (p == NULL || *p == '\0') {
735 pw = getpwuid(getuid());
736 if (pw != NULL)
737 p = pw->pw_name;
738 else
739 p = " ???";
741 l = strlen(p);
742 if (l >= sizeof(u))
743 l = sizeof(u) - 1;
744 memcpy(u, p, l);
745 u[l] = '\0';
746 return (u);
749 /*
750 * Score comparison function for qsort.
752 * If two scores are equal, the person who had the score first is
753 * listed first in the highscore file.
754 */
755 static int
756 cmpscores(const void *x, const void *y)
758 const struct highscore *a, *b;
759 long l;
761 a = x;
762 b = y;
763 l = (long)b->hs_level * b->hs_score - (long)a->hs_level * a->hs_score;
764 if (l < 0)
765 return (-1);
766 if (l > 0)
767 return (1);
768 if (a->hs_time < b->hs_time)
769 return (-1);
770 if (a->hs_time > b->hs_time)
771 return (1);
772 return (0);
775 /*
776 * If we've added a score to the file, we need to check the file and ensure
777 * that this player has only a few entries. The number of entries is
778 * controlled by MAXSCORES, and is to ensure that the highscore file is not
779 * monopolised by just a few people. People who no longer have accounts are
780 * only allowed the highest score. Scores older than EXPIRATION seconds are
781 * removed, unless they are someone's personal best.
782 * Caveat: the highest score on each level is always kept.
783 */
784 static int
785 checkscores(struct highscore *hs, int num)
787 struct highscore *sp;
788 int i, j, k, numnames;
789 int levelfound[NLEVELS];
790 struct peruser {
791 char *name;
792 int times;
793 } count[NUMSPOTS];
794 struct peruser *pu;
796 /*
797 * Sort so that highest totals come first.
799 * levelfound[i] becomes set when the first high score for that
800 * level is encountered. By definition this is the highest score.
801 */
802 qsort((void *)hs, nscores, sizeof(*hs), cmpscores);
803 for (i = MINLEVEL; i < NLEVELS; i++)
804 levelfound[i] = 0;
805 numnames = 0;
806 for (i = 0, sp = hs; i < num;) {
807 /*
808 * This is O(n^2), but do you think we care?
809 */
810 for (j = 0, pu = count; j < numnames; j++, pu++)
811 if (strcmp(sp->hs_name, pu->name) == 0)
812 break;
813 if (j == numnames) {
814 /*
815 * Add new user, set per-user count to 1.
816 */
817 pu->name = sp->hs_name;
818 pu->times = 1;
819 numnames++;
820 } else {
821 /*
822 * Two ways to keep this score:
823 * - Not too many (per user), still has acct, &
824 * score not dated; or
825 * - High score on this level.
826 */
827 if ((pu->times < MAXSCORES &&
828 getpwnam(sp->hs_name) != NULL &&
829 sp->hs_time + EXPIRATION >= now) ||
830 levelfound[sp->hs_level] == 0)
831 pu->times++;
832 else {
833 /*
834 * Delete this score, do not count it,
835 * do not pass go, do not collect $200.
836 */
837 num--;
838 for (k = i; k < num; k++)
839 hs[k] = hs[k + 1];
840 continue;
843 if (sp->hs_level < NLEVELS && sp->hs_level >= 0)
844 levelfound[sp->hs_level] = 1;
845 i++, sp++;
847 return (num > MAXHISCORES ? MAXHISCORES : num);
850 /*
851 * Show current scores. This must be called after savescore, if
852 * savescore is called at all, for two reasons:
853 * - Showscores munches the time field.
854 * - Even if that were not the case, a new score must be recorded
855 * before it can be shown anyway.
856 */
857 void
858 showscores(int level)
860 struct highscore *sp;
861 int i, n, c;
862 const char *me;
863 int levelfound[NLEVELS];
865 if (!gotscores)
866 getscores(NULL);
867 (void)printf("\n\t\t\t Tetris High Scores\n");
869 /*
870 * If level == 0, the person has not played a game but just asked for
871 * the high scores; we do not need to check for printing in highlight
872 * mode. If SOstr is null, we can't do highlighting anyway.
873 */
874 me = level && enter_standout_mode ? thisuser() : NULL;
876 /*
877 * Set times to 0 except for high score on each level.
878 */
879 for (i = MINLEVEL; i < NLEVELS; i++)
880 levelfound[i] = 0;
881 for (i = 0, sp = scores; i < nscores; i++, sp++) {
882 if (sp->hs_level < NLEVELS && sp->hs_level >= 0) {
883 if (levelfound[sp->hs_level])
884 sp->hs_time = 0;
885 else {
886 sp->hs_time = 1;
887 levelfound[sp->hs_level] = 1;
892 /*
893 * Page each screenful of scores.
894 */
895 for (i = 0, sp = scores; i < nscores; sp += n) {
896 n = 40;
897 if (i + n > nscores)
898 n = nscores - i;
899 printem(level, i + 1, sp, n, me);
900 if ((i += n) < nscores) {
901 (void)printf("\nHit RETURN to continue.");
902 (void)fflush(stdout);
903 while ((c = getchar()) != '\n')
904 if (c == EOF)
905 break;
906 (void)printf("\n");
911 static void
912 printem(int level, int offset, struct highscore *hs, int n, const char *me)
914 struct highscore *sp;
915 int nrows, row, col, item, i, highlight;
916 char buf[100];
917 #define TITLE "Rank Score Name (points/level)"
919 /*
920 * This makes a nice two-column sort with headers, but it's a bit
921 * convoluted...
922 */
923 printf("%s %s\n", TITLE, n > 1 ? TITLE : "");
925 highlight = 0;
926 nrows = (n + 1) / 2;
928 for (row = 0; row < nrows; row++) {
929 for (col = 0; col < 2; col++) {
930 item = col * nrows + row;
931 if (item >= n) {
932 /*
933 * Can only occur on trailing columns.
934 */
935 (void)putchar('\n');
936 continue;
938 sp = &hs[item];
939 (void)snprintf(buf, sizeof(buf),
940 "%3d%c %6d %-11s (%6d on %d)",
941 item + offset, sp->hs_time ? '*' : ' ',
942 sp->hs_score * sp->hs_level,
943 sp->hs_name, sp->hs_score, sp->hs_level);
944 /*
945 * Highlight if appropriate. This works because
946 * we only get one score per level.
947 */
948 if (me != NULL &&
949 sp->hs_level == level &&
950 sp->hs_score == score &&
951 strcmp(sp->hs_name, me) == 0) {
952 putpad(enter_standout_mode);
953 highlight = 1;
955 (void)printf("%s", buf);
956 if (highlight) {
957 putpad(exit_standout_mode);
958 highlight = 0;
961 /* fill in spaces so column 1 lines up */
962 if (col == 0)
963 for (i = 40 - strlen(buf); --i >= 0;)
964 (void)putchar(' ');
965 else /* col == 1 */
966 (void)putchar('\n');