1 /* $OpenBSD: scores.c,v 1.25 2019/06/28 13:32:52 deraadt 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 int nscores;
73 static int gotscores;
74 static struct highscore scores[NUMSPOTS];
75
76 static int checkscores(struct highscore *, 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, i;
95 char *mstr, *human;
96 FILE *sf;
97
98 if (fpp != NULL) {
99 mint = O_RDWR | O_CREAT;
100 mstr = "r+";
101 human = "read/write";
102 *fpp = NULL;
103 } else {
104 mint = O_RDONLY;
105 mstr = "r";
106 human = "reading";
107 }
108
109 sd = open(scorepath, mint, 0666);
110 if (sd == -1) {
111 if (fpp == NULL) {
112 nscores = 0;
113 return;
114 }
115 err(1, "cannot open %s for %s", scorepath, human);
116 }
117 if ((sf = fdopen(sd, mstr)) == NULL)
118 err(1, "cannot fdopen %s for %s", scorepath, human);
119
120 nscores = fread(scores, sizeof(scores[0]), MAXHISCORES, sf);
121 if (ferror(sf))
122 err(1, "error reading %s", scorepath);
123 for (i = 0; i < nscores; i++)
124 if (scores[i].hs_level < MINLEVEL ||
125 scores[i].hs_level > MAXLEVEL)
126 errx(1, "scorefile %s corrupt", scorepath);
127
128 if (fpp)
129 *fpp = sf;
130 else
131 (void)fclose(sf);
132 }
133
134 void
savescore(int level)135 savescore(int level)
136 {
137 struct highscore *sp;
138 int i;
139 int change;
140 FILE *sf;
141 const char *me;
142
143 getscores(&sf);
144 gotscores = 1;
145 (void)time(&now);
146
147 /*
148 * Allow at most one score per person per level -- see if we
149 * can replace an existing score, or (easiest) do nothing.
150 * Otherwise add new score at end (there is always room).
151 */
152 change = 0;
153 me = thisuser();
154 for (i = 0, sp = &scores[0]; i < nscores; i++, sp++) {
155 if (sp->hs_level != level || strcmp(sp->hs_name, me) != 0)
156 continue;
157 if (score > sp->hs_score) {
158 (void)printf("%s bettered %s %d score of %d!\n",
159 "\nYou", "your old level", level,
160 sp->hs_score * sp->hs_level);
161 sp->hs_score = score; /* new score */
162 sp->hs_time = now; /* and time */
163 change = 1;
164 } else if (score == sp->hs_score) {
165 (void)printf("%s tied %s %d high score.\n",
166 "\nYou", "your old level", level);
167 sp->hs_time = now; /* renew it */
168 change = 1; /* gotta rewrite, sigh */
169 } /* else new score < old score: do nothing */
170 break;
171 }
172 if (i >= nscores) {
173 strlcpy(sp->hs_name, me, sizeof sp->hs_name);
174 sp->hs_level = level;
175 sp->hs_score = score;
176 sp->hs_time = now;
177 nscores++;
178 change = 1;
179 }
180
181 if (change) {
182 /*
183 * Sort & clean the scores, then rewrite.
184 */
185 nscores = checkscores(scores, nscores);
186 if (fseek(sf, 0L, SEEK_SET) == -1)
187 err(1, "fseek");
188 if (fwrite(scores, sizeof(*sp), nscores, sf) != nscores ||
189 fflush(sf) == EOF)
190 warnx("error writing scorefile: %s\n\t-- %s",
191 strerror(errno),
192 "high scores may be damaged");
193 }
194 (void)fclose(sf); /* releases lock */
195 }
196
197 /*
198 * Get login name, or if that fails, get something suitable.
199 * The result is always trimmed to fit in a score.
200 */
201 static char *
thisuser(void)202 thisuser(void)
203 {
204 const char *p;
205 static char u[sizeof(scores[0].hs_name)];
206
207 if (u[0])
208 return (u);
209 p = getenv("LOGNAME");
210 if (p == NULL || *p == '\0')
211 p = getenv("USER");
212 if (p == NULL || *p == '\0')
213 p = getlogin();
214 if (p == NULL || *p == '\0')
215 p = " ???";
216 strlcpy(u, p, sizeof(u));
217 return (u);
218 }
219
220 /*
221 * Score comparison function for qsort.
222 *
223 * If two scores are equal, the person who had the score first is
224 * listed first in the highscore file.
225 */
226 static int
cmpscores(const void * x,const void * y)227 cmpscores(const void *x, const void *y)
228 {
229 const struct highscore *a, *b;
230 long l;
231
232 a = x;
233 b = y;
234 l = (long)b->hs_level * b->hs_score - (long)a->hs_level * a->hs_score;
235 if (l < 0)
236 return (-1);
237 if (l > 0)
238 return (1);
239 if (a->hs_time < b->hs_time)
240 return (-1);
241 if (a->hs_time > b->hs_time)
242 return (1);
243 return (0);
244 }
245
246 /*
247 * If we've added a score to the file, we need to check the file and ensure
248 * that this player has only a few entries. The number of entries is
249 * controlled by MAXSCORES, and is to ensure that the highscore file is not
250 * monopolised by just a few people. People who no longer have accounts are
251 * only allowed the highest score. Scores older than EXPIRATION seconds are
252 * removed, unless they are someone's personal best.
253 * Caveat: the highest score on each level is always kept.
254 */
255 static int
checkscores(struct highscore * hs,int num)256 checkscores(struct highscore *hs, int num)
257 {
258 struct highscore *sp;
259 int i, j, k, nrnames;
260 int levelfound[NLEVELS];
261 struct peruser {
262 char *name;
263 int times;
264 } count[NUMSPOTS];
265 struct peruser *pu;
266
267 /*
268 * Sort so that highest totals come first.
269 *
270 * levelfound[i] becomes set when the first high score for that
271 * level is encountered. By definition this is the highest score.
272 */
273 qsort((void *)hs, nscores, sizeof(*hs), cmpscores);
274 for (i = MINLEVEL; i < NLEVELS; i++)
275 levelfound[i] = 0;
276 nrnames = 0;
277 for (i = 0, sp = hs; i < num;) {
278 /*
279 * This is O(n^2), but do you think we care?
280 */
281 for (j = 0, pu = count; j < nrnames; j++, pu++)
282 if (strcmp(sp->hs_name, pu->name) == 0)
283 break;
284 if (j == nrnames) {
285 /*
286 * Add new user, set per-user count to 1.
287 */
288 pu->name = sp->hs_name;
289 pu->times = 1;
290 nrnames++;
291 } else {
292 /*
293 * Two ways to keep this score:
294 * - Not too many (per user), still has acct, &
295 * score not dated; or
296 * - High score on this level.
297 */
298 if ((pu->times < MAXSCORES &&
299 sp->hs_time + EXPIRATION >= now) ||
300 levelfound[sp->hs_level] == 0)
301 pu->times++;
302 else {
303 /*
304 * Delete this score, do not count it,
305 * do not pass go, do not collect $200.
306 */
307 num--;
308 for (k = i; k < num; k++)
309 hs[k] = hs[k + 1];
310 continue;
311 }
312 }
313 levelfound[sp->hs_level] = 1;
314 i++, sp++;
315 }
316 return (num > MAXHISCORES ? MAXHISCORES : num);
317 }
318
319 /*
320 * Show current scores. This must be called after savescore, if
321 * savescore is called at all, for two reasons:
322 * - Showscores munches the time field.
323 * - Even if that were not the case, a new score must be recorded
324 * before it can be shown anyway.
325 */
326 void
showscores(int level)327 showscores(int level)
328 {
329 struct highscore *sp;
330 int i, n, c;
331 const char *me;
332 int levelfound[NLEVELS];
333
334 if (!gotscores)
335 getscores((FILE **)NULL);
336 (void)printf("\n\t\t Tetris High Scores\n");
337
338 /*
339 * If level == 0, the person has not played a game but just asked for
340 * the high scores; we do not need to check for printing in highlight
341 * mode. If SOstr is null, we can't do highlighting anyway.
342 */
343 me = level && SOstr ? thisuser() : NULL;
344
345 /*
346 * Set times to 0 except for high score on each level.
347 */
348 for (i = MINLEVEL; i < NLEVELS; i++)
349 levelfound[i] = 0;
350 for (i = 0, sp = scores; i < nscores; i++, sp++) {
351 if (levelfound[sp->hs_level])
352 sp->hs_time = 0;
353 else {
354 sp->hs_time = 1;
355 levelfound[sp->hs_level] = 1;
356 }
357 }
358
359 /*
360 * Page each screenful of scores.
361 */
362 for (i = 0, sp = scores; i < nscores; sp += n) {
363 n = 20;
364 if (i + n > nscores)
365 n = nscores - i;
366 printem(level, i + 1, sp, n, me);
367 if ((i += n) < nscores) {
368 (void)printf("\nHit RETURN to continue.");
369 (void)fflush(stdout);
370 while ((c = getchar()) != '\n')
371 if (c == EOF)
372 break;
373 (void)printf("\n");
374 }
375 }
376
377 if (nscores == 0)
378 printf("\t\t\t - none to date.\n");
379 }
380
381 static void
printem(int level,int offset,struct highscore * hs,int n,const char * me)382 printem(int level, int offset, struct highscore *hs, int n, const char *me)
383 {
384 struct highscore *sp;
385 int row, highlight, i;
386 char buf[100];
387 #define TITLE "Rank Score Name (points/level)"
388 #define TITL2 "=========================================================="
389
390 printf("%s\n%s\n", TITLE, TITL2);
391
392 highlight = 0;
393
394 for (row = 0; row < n; row++) {
395 sp = &hs[row];
396 (void)snprintf(buf, sizeof(buf),
397 "%3d%c %6d %-31s (%6d on %d)\n",
398 row + offset, sp->hs_time ? '*' : ' ',
399 sp->hs_score * sp->hs_level,
400 sp->hs_name, sp->hs_score, sp->hs_level);
401 /* Print leaders every three lines */
402 if ((row + 1) % 3 == 0) {
403 for (i = 0; i < sizeof(buf); i++)
404 if (buf[i] == ' ')
405 buf[i] = '_';
406 }
407 /*
408 * Highlight if appropriate. This works because
409 * we only get one score per level.
410 */
411 if (me != NULL &&
412 sp->hs_level == level &&
413 sp->hs_score == score &&
414 strcmp(sp->hs_name, me) == 0) {
415 putpad(SOstr);
416 highlight = 1;
417 }
418 (void)printf("%s", buf);
419 if (highlight) {
420 putpad(SEstr);
421 highlight = 0;
422 }
423 }
424 }
425