xref: /dragonfly/games/tetris/scores.c (revision 03fc749469d95e31f3f374d40b0903dd9e095295)
1 /*        $OpenBSD: scores.c,v 1.22 2016/08/27 02:00:10 guenther Exp $          */
2 /*        $NetBSD: scores.c,v 1.2 1995/04/22 07:42:38 cgd Exp $       */
3 
4 /*-
5  * Copyright (c) 1992, 1993
6  *        The Regents of the University of California.  All rights reserved.
7  *
8  * This code is derived from software contributed to Berkeley by
9  * Chris Torek and Darren F. Provine.
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions
13  * are met:
14  * 1. Redistributions of source code must retain the above copyright
15  *    notice, this list of conditions and the following disclaimer.
16  * 2. Redistributions in binary form must reproduce the above copyright
17  *    notice, this list of conditions and the following disclaimer in the
18  *    documentation and/or other materials provided with the distribution.
19  * 3. Neither the name of the University nor the names of its contributors
20  *    may be used to endorse or promote products derived from this software
21  *    without specific prior written permission.
22  *
23  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
24  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
27  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
29  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
32  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33  * SUCH DAMAGE.
34  *
35  *        @(#)scores.c        8.1 (Berkeley) 5/31/93
36  */
37 
38 /*
39  * Score code for Tetris, by Darren Provine (kilroy@gboro.glassboro.edu)
40  * modified 22 January 1992, to limit the number of entries any one
41  * person has.
42  *
43  * Major whacks since then.
44  */
45 #include <err.h>
46 #include <errno.h>
47 #include <fcntl.h>
48 #include <limits.h>
49 #include <stdio.h>
50 #include <stdlib.h>
51 #include <string.h>
52 #include <term.h>
53 #include <time.h>
54 #include <unistd.h>
55 
56 #include "scores.h"
57 #include "screen.h"
58 #include "tetris.h"
59 
60 /*
61  * Within this code, we can hang onto one extra "high score", leaving
62  * room for our current score (whether or not it is high).
63  *
64  * We also sometimes keep tabs on the "highest" score on each level.
65  * As long as the scores are kept sorted, this is simply the first one at
66  * that level.
67  */
68 #define NUMSPOTS (MAXHISCORES + 1)
69 #define   NLEVELS (MAXLEVEL + 1)
70 
71 static time_t now;
72 static unsigned int nscores;
73 static int gotscores;
74 static struct highscore scores[NUMSPOTS];
75 
76 static int checkscores(struct highscore *, unsigned int);
77 static int cmpscores(const void *, const void *);
78 static void getscores(FILE **);
79 static void printem(int, int, struct highscore *, int, const char *);
80 static char *thisuser(void);
81 
82 /*
83  * Read the score file.  Can be called from savescore (before showscores)
84  * or showscores (if savescore will not be called).  If the given pointer
85  * is not NULL, sets *fpp to an open file pointer that corresponds to a
86  * read/write score file that is locked with LOCK_EX.  Otherwise, the
87  * file is locked with LOCK_SH for the read and closed before return.
88  *
89  * Note, we assume closing the stdio file releases the lock.
90  */
91 static void
getscores(FILE ** fpp)92 getscores(FILE **fpp)
93 {
94           int sd, mint, ret;
95           unsigned int i;
96           const char *mstr, *human, *home;
97           char scorepath[PATH_MAX];
98           FILE *sf;
99 
100           if (fpp != NULL) {
101                     mint = O_RDWR | O_CREAT;
102                     mstr = "r+";
103                     human = "read/write";
104                     *fpp = NULL;
105           } else {
106                     mint = O_RDONLY;
107                     mstr = "r";
108                     human = "reading";
109           }
110 
111           home = getenv("HOME");
112           if (home == NULL || *home == '\0')
113                     err(1, "getenv");
114 
115           ret = snprintf(scorepath, sizeof(scorepath), "%s/%s", home, ".tetris.scores");
116           if (ret < 0 || ret >= PATH_MAX)
117                     errc(1, ENAMETOOLONG, "%s/%s", home, ".tetris.scores");
118 
119           sd = open(scorepath, mint, 0666);
120           if (sd < 0) {
121                     if (fpp == NULL) {
122                               nscores = 0;
123                               return;
124                     }
125                     err(1, "cannot open %s for %s", scorepath, human);
126           }
127           if ((sf = fdopen(sd, mstr)) == NULL)
128                     err(1, "cannot fdopen %s for %s", scorepath, human);
129 
130           nscores = fread(scores, sizeof(scores[0]), MAXHISCORES, sf);
131           if (ferror(sf))
132                     err(1, "error reading %s", scorepath);
133           for (i = 0; i < nscores; i++)
134                     if (scores[i].hs_level < MINLEVEL ||
135                         scores[i].hs_level > MAXLEVEL)
136                               errx(1, "scorefile %s corrupt", scorepath);
137 
138           if (fpp)
139                     *fpp = sf;
140           else
141                     fclose(sf);
142 }
143 
144 void
savescore(int level)145 savescore(int level)
146 {
147           struct highscore *sp;
148           unsigned int i;
149           int change;
150           FILE *sf;
151           const char *me;
152 
153           getscores(&sf);
154           gotscores = 1;
155           time(&now);
156 
157           /*
158            * Allow at most one score per person per level -- see if we
159            * can replace an existing score, or (easiest) do nothing.
160            * Otherwise add new score at end (there is always room).
161            */
162           change = 0;
163           me = thisuser();
164           for (i = 0, sp = &scores[0]; i < nscores; i++, sp++) {
165                     if (sp->hs_level != level || strcmp(sp->hs_name, me) != 0)
166                               continue;
167                     if (score > sp->hs_score) {
168                               printf("%s bettered %s %d score of %d!\n",
169                                   "\nYou", "your old level", level,
170                                   sp->hs_score * sp->hs_level);
171                               sp->hs_score = score;         /* new score */
172                               sp->hs_time = now;  /* and time */
173                               change = 1;
174                     } else if (score == sp->hs_score) {
175                               printf("%s tied %s %d high score.\n",
176                                   "\nYou", "your old level", level);
177                               sp->hs_time = now;  /* renew it */
178                               change = 1;                   /* gotta rewrite, sigh */
179                     } /* else new score < old score: do nothing */
180                     break;
181           }
182           if (i >= nscores) {
183                     strlcpy(sp->hs_name, me, sizeof sp->hs_name);
184                     sp->hs_level = level;
185                     sp->hs_score = score;
186                     sp->hs_time = now;
187                     nscores++;
188                     change = 1;
189           }
190 
191           if (change) {
192                     /*
193                      * Sort & clean the scores, then rewrite.
194                      */
195                     nscores = checkscores(scores, nscores);
196                     if (fseek(sf, 0L, SEEK_SET) == -1)
197                               err(1, "fseek");
198                     if (fwrite(scores, sizeof(*sp), nscores, sf) != nscores ||
199                         fflush(sf) == EOF)
200                               warnx("error writing scorefile: %s\n\t-- %s",
201                                   strerror(errno),
202                                   "high scores may be damaged");
203           }
204           fclose(sf);         /* releases lock */
205 }
206 
207 /*
208  * Get login name, or if that fails, get something suitable.
209  * The result is always trimmed to fit in a score.
210  */
211 static char *
thisuser(void)212 thisuser(void)
213 {
214           const char *p;
215           static char u[sizeof(scores[0].hs_name)];
216 
217           if (u[0])
218                     return (u);
219           p = getenv("LOGNAME");
220           if (p == NULL || *p == '\0')
221                     p = getenv("USER");
222           if (p == NULL || *p == '\0')
223                     p = getlogin();
224           if (p == NULL || *p == '\0')
225                     p = "  ???";
226           strlcpy(u, p, sizeof(u));
227           return (u);
228 }
229 
230 /*
231  * Score comparison function for qsort.
232  *
233  * If two scores are equal, the person who had the score first is
234  * listed first in the highscore file.
235  */
236 static int
cmpscores(const void * x,const void * y)237 cmpscores(const void *x, const void *y)
238 {
239           const struct highscore *a, *b;
240           long l;
241 
242           a = x;
243           b = y;
244           l = (long)b->hs_level * b->hs_score - (long)a->hs_level * a->hs_score;
245           if (l < 0)
246                     return (-1);
247           if (l > 0)
248                     return (1);
249           if (a->hs_time < b->hs_time)
250                     return (-1);
251           if (a->hs_time > b->hs_time)
252                     return (1);
253           return (0);
254 }
255 
256 /*
257  * If we've added a score to the file, we need to check the file and ensure
258  * that this player has only a few entries.  The number of entries is
259  * controlled by MAXSCORES, and is to ensure that the highscore file is not
260  * monopolised by just a few people.  People who no longer have accounts are
261  * only allowed the highest score.  Scores older than EXPIRATION seconds are
262  * removed, unless they are someone's personal best.
263  * Caveat:  the highest score on each level is always kept.
264  */
265 static int
checkscores(struct highscore * hs,unsigned int num)266 checkscores(struct highscore *hs, unsigned int num)
267 {
268           struct highscore *sp;
269           unsigned int i, j, k, nrnames;
270           int levelfound[NLEVELS];
271           struct peruser {
272                     char *name;
273                     int times;
274           } count[NUMSPOTS];
275           struct peruser *pu;
276 
277           /*
278            * Sort so that highest totals come first.
279            *
280            * levelfound[i] becomes set when the first high score for that
281            * level is encountered.  By definition this is the highest score.
282            */
283           qsort((void *)hs, nscores, sizeof(*hs), cmpscores);
284           for (i = MINLEVEL; i < NLEVELS; i++)
285                     levelfound[i] = 0;
286           nrnames = 0;
287           for (i = 0, sp = hs; i < num;) {
288                     /*
289                      * This is O(n^2), but do you think we care?
290                      */
291                     for (j = 0, pu = count; j < nrnames; j++, pu++)
292                               if (strcmp(sp->hs_name, pu->name) == 0)
293                                         break;
294                     if (j == nrnames) {
295                               /*
296                                * Add new user, set per-user count to 1.
297                                */
298                               pu->name = sp->hs_name;
299                               pu->times = 1;
300                               nrnames++;
301                     } else {
302                               /*
303                                * Two ways to keep this score:
304                                * - Not too many (per user), still has acct, &
305                                *        score not dated; or
306                                * - High score on this level.
307                                */
308                               if ((pu->times < MAXSCORES &&
309                                    sp->hs_time + EXPIRATION >= now) ||
310                                   levelfound[sp->hs_level] == 0)
311                                         pu->times++;
312                               else {
313                                         /*
314                                          * Delete this score, do not count it,
315                                          * do not pass go, do not collect $200.
316                                          */
317                                         num--;
318                                         for (k = i; k < num; k++)
319                                                   hs[k] = hs[k + 1];
320                                         continue;
321                               }
322                     }
323                     levelfound[sp->hs_level] = 1;
324                     i++, sp++;
325           }
326           return (num > MAXHISCORES ? MAXHISCORES : num);
327 }
328 
329 /*
330  * Show current scores.  This must be called after savescore, if
331  * savescore is called at all, for two reasons:
332  * - Showscores munches the time field.
333  * - Even if that were not the case, a new score must be recorded
334  *   before it can be shown anyway.
335  */
336 void
showscores(int level)337 showscores(int level)
338 {
339           struct highscore *sp;
340           unsigned int i, n;
341           int c;
342           const char *me;
343           int levelfound[NLEVELS];
344 
345           if (!gotscores)
346                     getscores((FILE **)NULL);
347           printf("\n\t\t    Tetris High Scores\n");
348 
349           /*
350            * If level == 0, the person has not played a game but just asked for
351            * the high scores; we do not need to check for printing in highlight
352            * mode.  If SOstr is null, we can't do highlighting anyway.
353            */
354           me = level && SOstr ? thisuser() : NULL;
355 
356           /*
357            * Set times to 0 except for high score on each level.
358            */
359           for (i = MINLEVEL; i < NLEVELS; i++)
360                     levelfound[i] = 0;
361           for (i = 0, sp = scores; i < nscores; i++, sp++) {
362                     if (levelfound[sp->hs_level])
363                               sp->hs_time = 0;
364                     else {
365                               sp->hs_time = 1;
366                               levelfound[sp->hs_level] = 1;
367                     }
368           }
369 
370           /*
371            * Page each screenful of scores.
372            */
373           for (i = 0, sp = scores; i < nscores; sp += n) {
374                     n = 20;
375                     if (i + n > nscores)
376                               n = nscores - i;
377                     printem(level, i + 1, sp, n, me);
378                     if ((i += n) < nscores) {
379                               printf("\nHit RETURN to continue.");
380                               fflush(stdout);
381                               while ((c = getchar()) != '\n')
382                                         if (c == EOF)
383                                                   break;
384                               printf("\n");
385                     }
386           }
387 
388           if (nscores == 0)
389                     printf("\t\t\t      - none to date.\n");
390 }
391 
392 static void
printem(int level,int offset,struct highscore * hs,int n,const char * me)393 printem(int level, int offset, struct highscore *hs, int n, const char *me)
394 {
395           struct highscore *sp;
396           int row, highlight;
397           unsigned int i;
398           char buf[100];
399 #define   TITLE "Rank  Score   Name                          (points/level)"
400 #define   TITL2 "=========================================================="
401 
402           printf("%s\n%s\n", TITLE, TITL2);
403 
404           highlight = 0;
405 
406           for (row = 0; row < n; row++) {
407                     sp = &hs[row];
408                     snprintf(buf, sizeof(buf),
409                         "%3d%c %6d  %-31s (%6d on %d)\n",
410                         row + offset, sp->hs_time ? '*' : ' ',
411                         sp->hs_score * sp->hs_level,
412                         sp->hs_name, sp->hs_score, sp->hs_level);
413                     /* Print leaders every three lines */
414                     if ((row + 1) % 3 == 0) {
415                               for (i = 0; i < sizeof(buf); i++)
416                                         if (buf[i] == ' ')
417                                                   buf[i] = '_';
418                     }
419                     /*
420                      * Highlight if appropriate.  This works because
421                      * we only get one score per level.
422                      */
423                     if (me != NULL &&
424                         sp->hs_level == level &&
425                         sp->hs_score == score &&
426                         strcmp(sp->hs_name, me) == 0) {
427                               putpad(SOstr);
428                               highlight = 1;
429                     }
430                     printf("%s", buf);
431                     if (highlight) {
432                               putpad(SEstr);
433                               highlight = 0;
434                     }
435           }
436 }
437