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