Commit | Line | Data |
---|---|---|
4dbbbd26 KB |
1 | /*- |
2 | * Copyright (c) 1992 The Regents of the University of California. | |
3 | * All rights reserved. | |
4 | * | |
5 | * %sccs.include.redist.c% | |
6 | * | |
7 | * @(#)scores.c 5.1 (Berkeley) %G% | |
8 | */ | |
9 | ||
10 | /* | |
11 | * score code for Tetris, by Darren Provine (kilroy@gboro.glassboro.edu) | |
12 | * modified 22 January 1992, to limit the number of entries any one | |
13 | * person has. | |
14 | * | |
15 | * Major whacks since then. | |
16 | */ | |
17 | #include <errno.h> | |
18 | #include <fcntl.h> | |
19 | #include <pwd.h> | |
20 | #include <stdio.h> | |
21 | #include <stdlib.h> | |
22 | #include <string.h> | |
23 | #include <time.h> | |
24 | #include <unistd.h> | |
25 | ||
26 | /* | |
27 | * XXX - need a <termcap.h> | |
28 | */ | |
29 | int tputs __P((const char *, int, int (*)(int))); | |
30 | ||
31 | #include "pathnames.h" | |
32 | #include "screen.h" | |
33 | #include "scores.h" | |
34 | #include "tetris.h" | |
35 | ||
36 | /* | |
37 | * Within this code, we can hang onto one extra "high score", leaving | |
38 | * room for our current score (whether or not it is high). | |
39 | * | |
40 | * We also sometimes keep tabs on the "highest" score on each level. | |
41 | * As long as the scores are kept sorted, this is simply the first one at | |
42 | * that level. | |
43 | */ | |
44 | #define NUMSPOTS (MAXHISCORES + 1) | |
45 | #define NLEVELS (MAXLEVEL + 1) | |
46 | ||
47 | static time_t now; | |
48 | static int nscores; | |
49 | static int gotscores; | |
50 | static struct highscore scores[NUMSPOTS]; | |
51 | ||
52 | static int checkscores __P((struct highscore *, int)); | |
53 | static int cmpscores __P((const void *, const void *)); | |
54 | static void getscores __P((FILE **)); | |
55 | static void printem __P((int, int, struct highscore *, int, const char *)); | |
56 | static char *thisuser __P((void)); | |
57 | ||
58 | /* | |
59 | * Read the score file. Can be called from savescore (before showscores) | |
60 | * or showscores (if savescore will not be called). If the given pointer | |
61 | * is not NULL, sets *fpp to an open file pointer that corresponds to a | |
62 | * read/write score file that is locked with LOCK_EX. Otherwise, the | |
63 | * file is locked with LOCK_SH for the read and closed before return. | |
64 | * | |
65 | * Note, we assume closing the stdio file releases the lock. | |
66 | */ | |
67 | static void | |
68 | getscores(fpp) | |
69 | FILE **fpp; | |
70 | { | |
71 | int sd, mint, lck; | |
72 | char *mstr, *human; | |
73 | FILE *sf; | |
74 | ||
75 | if (fpp != NULL) { | |
76 | mint = O_RDWR | O_CREAT; | |
77 | mstr = "r+"; | |
78 | human = "read/write"; | |
79 | lck = LOCK_EX; | |
80 | } else { | |
81 | mint = O_RDONLY; | |
82 | mstr = "r"; | |
83 | human = "reading"; | |
84 | lck = LOCK_SH; | |
85 | } | |
86 | sd = open(_PATH_SCOREFILE, mint, 0666); | |
87 | if (sd < 0) { | |
88 | if (fpp == NULL) { | |
89 | nscores = 0; | |
90 | return; | |
91 | } | |
92 | (void)fprintf(stderr, "tetris: cannot open %s for %s: %s\n", | |
93 | _PATH_SCOREFILE, human, strerror(errno)); | |
94 | exit(1); | |
95 | } | |
96 | if ((sf = fdopen(sd, mstr)) == NULL) { | |
97 | (void)fprintf(stderr, "tetris: cannot fdopen %s for %s: %s\n", | |
98 | _PATH_SCOREFILE, human, strerror(errno)); | |
99 | exit(1); | |
100 | } | |
101 | ||
102 | /* | |
103 | * Grab a lock. | |
104 | */ | |
105 | if (flock(sd, lck)) | |
106 | (void)fprintf(stderr, | |
107 | "tetris: warning: score file %s cannot be locked: %s\n", | |
108 | _PATH_SCOREFILE, strerror(errno)); | |
109 | ||
110 | nscores = fread(scores, sizeof(scores[0]), MAXHISCORES, sf); | |
111 | if (ferror(sf)) { | |
112 | (void)fprintf(stderr, "tetris: error reading %s: %s\n", | |
113 | _PATH_SCOREFILE, strerror(errno)); | |
114 | exit(1); | |
115 | } | |
116 | ||
117 | if (fpp) | |
118 | *fpp = sf; | |
119 | else | |
120 | (void)fclose(sf); | |
121 | } | |
122 | ||
123 | void | |
124 | savescore(level) | |
125 | int level; | |
126 | { | |
127 | register struct highscore *sp; | |
128 | register int i; | |
129 | int change; | |
130 | FILE *sf; | |
131 | const char *me; | |
132 | ||
133 | getscores(&sf); | |
134 | gotscores = 1; | |
135 | (void)time(&now); | |
136 | ||
137 | /* | |
138 | * Allow at most one score per person per level -- see if we | |
139 | * can replace an existing score, or (easiest) do nothing. | |
140 | * Otherwise add new score at end (there is always room). | |
141 | */ | |
142 | change = 0; | |
143 | me = thisuser(); | |
144 | for (i = 0, sp = &scores[0]; i < nscores; i++, sp++) { | |
145 | if (sp->hs_level != level || strcmp(sp->hs_name, me) != 0) | |
146 | continue; | |
147 | if (score > sp->hs_score) { | |
148 | (void)printf("%s bettered %s %d score of %d!\n", | |
149 | "\nYou", "your old level", level, | |
150 | sp->hs_score * sp->hs_level); | |
151 | sp->hs_score = score; /* new score */ | |
152 | sp->hs_time = now; /* and time */ | |
153 | change = 1; | |
154 | } else if (score == sp->hs_score) { | |
155 | (void)printf("%s tied %s %d high score.\n", | |
156 | "\nYou", "your old level", level); | |
157 | sp->hs_time = now; /* renew it */ | |
158 | change = 1; /* gotta rewrite, sigh */ | |
159 | } /* else new score < old score: do nothing */ | |
160 | break; | |
161 | } | |
162 | if (i >= nscores) { | |
163 | strcpy(sp->hs_name, me); | |
164 | sp->hs_level = level; | |
165 | sp->hs_score = score; | |
166 | sp->hs_time = now; | |
167 | nscores++; | |
168 | change = 1; | |
169 | } | |
170 | ||
171 | if (change) { | |
172 | /* | |
173 | * Sort & clean the scores, then rewrite. | |
174 | */ | |
175 | nscores = checkscores(scores, nscores); | |
176 | rewind(sf); | |
177 | if (fwrite(scores, sizeof(*sp), nscores, sf) != nscores || | |
178 | fflush(sf) == EOF) | |
179 | (void)fprintf(stderr, | |
180 | "tetris: error writing %s: %s -- %s\n", | |
181 | _PATH_SCOREFILE, strerror(errno), | |
182 | "high scores may be damaged"); | |
183 | } | |
184 | (void)fclose(sf); /* releases lock */ | |
185 | } | |
186 | ||
187 | /* | |
188 | * Get login name, or if that fails, get something suitable. | |
189 | * The result is always trimmed to fit in a score. | |
190 | */ | |
191 | static char * | |
192 | thisuser() | |
193 | { | |
194 | register const char *p; | |
195 | register struct passwd *pw; | |
196 | register size_t l; | |
197 | static char u[sizeof(scores[0].hs_name)]; | |
198 | ||
199 | if (u[0]) | |
200 | return (u); | |
201 | p = getlogin(); | |
202 | if (p == NULL || *p == '\0') { | |
203 | pw = getpwuid(getuid()); | |
204 | if (pw != NULL) | |
205 | p = pw->pw_name; | |
206 | else | |
207 | p = " ???"; | |
208 | } | |
209 | l = strlen(p); | |
210 | if (l >= sizeof(u)) | |
211 | l = sizeof(u) - 1; | |
212 | bcopy(p, u, l); | |
213 | u[l] = '\0'; | |
214 | return (u); | |
215 | } | |
216 | ||
217 | /* | |
218 | * Score comparison function for qsort. | |
219 | * | |
220 | * If two scores are equal, the person who had the score first is | |
221 | * listed first in the highscore file. | |
222 | */ | |
223 | static int | |
224 | cmpscores(x, y) | |
225 | const void *x, *y; | |
226 | { | |
227 | register const struct highscore *a, *b; | |
228 | register long l; | |
229 | ||
230 | a = x; | |
231 | b = y; | |
232 | l = (long)b->hs_level * b->hs_score - (long)a->hs_level * a->hs_score; | |
233 | if (l < 0) | |
234 | return (-1); | |
235 | if (l > 0) | |
236 | return (1); | |
237 | if (a->hs_time < b->hs_time) | |
238 | return (-1); | |
239 | if (a->hs_time > b->hs_time) | |
240 | return (1); | |
241 | return (0); | |
242 | } | |
243 | ||
244 | /* | |
245 | * If we've added a score to the file, we need to check the file and ensure | |
246 | * that this player has only a few entries. The number of entries is | |
247 | * controlled by MAXSCORES, and is to ensure that the highscore file is not | |
248 | * monopolised by just a few people. People who no longer have accounts are | |
249 | * only allowed the highest score. Scores older than EXPIRATION seconds are | |
250 | * removed, unless they are someone's personal best. | |
251 | * Caveat: the highest score on each level is always kept. | |
252 | */ | |
253 | static int | |
254 | checkscores(hs, num) | |
255 | register struct highscore *hs; | |
256 | int num; | |
257 | { | |
258 | register struct highscore *sp; | |
259 | register int i, j, k, numnames; | |
260 | int levelfound[NLEVELS]; | |
261 | struct peruser { | |
262 | char *name; | |
263 | int times; | |
264 | } count[NUMSPOTS]; | |
265 | register 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 | numnames = 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 < numnames; j++, pu++) | |
282 | if (strcmp(sp->hs_name, pu->name) == 0) | |
283 | break; | |
284 | if (j == numnames) { | |
285 | /* | |
286 | * Add new user, set per-user count to 1. | |
287 | */ | |
288 | pu->name = sp->hs_name; | |
289 | pu->times = 1; | |
290 | numnames++; | |
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 | getpwnam(sp->hs_name) != NULL && | |
300 | sp->hs_time + EXPIRATION >= now) || | |
301 | levelfound[sp->hs_level] == 0) | |
302 | pu->times++; | |
303 | else { | |
304 | /* | |
305 | * Delete this score, do not count it, | |
306 | * do not pass go, do not collect $200. | |
307 | */ | |
308 | num--; | |
309 | for (k = i; k < num; k++) | |
310 | hs[k] = hs[k + 1]; | |
311 | continue; | |
312 | } | |
313 | } | |
314 | levelfound[sp->hs_level] = 1; | |
315 | i++, sp++; | |
316 | } | |
317 | return (num > MAXHISCORES ? MAXHISCORES : num); | |
318 | } | |
319 | ||
320 | /* | |
321 | * Show current scores. This must be called after savescore, if | |
322 | * savescore is called at all, for two reasons: | |
323 | * - Showscores munches the time field. | |
324 | * - Even if that were not the case, a new score must be recorded | |
325 | * before it can be shown anyway. | |
326 | */ | |
327 | void | |
328 | showscores(level) | |
329 | int level; | |
330 | { | |
331 | register struct highscore *sp; | |
332 | register int i, n, c; | |
333 | const char *me; | |
334 | int levelfound[NLEVELS]; | |
335 | ||
336 | if (!gotscores) | |
337 | getscores((FILE **)NULL); | |
338 | (void)printf("\n\t\t\t Tetris High Scores\n"); | |
339 | ||
340 | /* | |
341 | * If level == 0, the person has not played a game but just asked for | |
342 | * the high scores; we do not need to check for printing in highlight | |
343 | * mode. If SOstr is null, we can't do highlighting anyway. | |
344 | */ | |
345 | me = level && SOstr ? thisuser() : NULL; | |
346 | ||
347 | /* | |
348 | * Set times to 0 except for high score on each level. | |
349 | */ | |
350 | for (i = MINLEVEL; i < NLEVELS; i++) | |
351 | levelfound[i] = 0; | |
352 | for (i = 0, sp = scores; i < nscores; i++, sp++) { | |
353 | if (levelfound[sp->hs_level]) | |
354 | sp->hs_time = 0; | |
355 | else { | |
356 | sp->hs_time = 1; | |
357 | levelfound[sp->hs_level] = 1; | |
358 | } | |
359 | } | |
360 | ||
361 | /* | |
362 | * Page each screenful of scores. | |
363 | */ | |
364 | for (i = 0, sp = scores; i < nscores; sp += n) { | |
365 | n = 40; | |
366 | if (i + n > nscores) | |
367 | n = nscores - i; | |
368 | printem(level, i + 1, sp, n, me); | |
369 | if ((i += n) < nscores) { | |
370 | (void)printf("\nHit RETURN to continue."); | |
371 | (void)fflush(stdout); | |
372 | while ((c = getchar()) != '\n') | |
373 | if (c == EOF) | |
374 | break; | |
375 | (void)printf("\n"); | |
376 | } | |
377 | } | |
378 | } | |
379 | ||
380 | static void | |
381 | printem(level, offset, hs, n, me) | |
382 | int level, offset; | |
383 | register struct highscore *hs; | |
384 | register int n; | |
385 | const char *me; | |
386 | { | |
387 | register struct highscore *sp; | |
388 | int nrows, row, col, item, i, highlight; | |
389 | char buf[100]; | |
390 | #define TITLE "Rank Score Name (points/level)" | |
391 | ||
392 | /* | |
393 | * This makes a nice two-column sort with headers, but it's a bit | |
394 | * convoluted... | |
395 | */ | |
396 | printf("%s %s\n", TITLE, n > 1 ? TITLE : ""); | |
397 | ||
398 | highlight = 0; | |
399 | nrows = (n + 1) / 2; | |
400 | ||
401 | for (row = 0; row < nrows; row++) { | |
402 | for (col = 0; col < 2; col++) { | |
403 | item = col * nrows + row; | |
404 | if (item >= n) { | |
405 | /* | |
406 | * Can only occur on trailing columns. | |
407 | */ | |
408 | (void)putchar('\n'); | |
409 | continue; | |
410 | } | |
411 | (void)printf(item + offset < 10 ? " " : " "); | |
412 | sp = &hs[item]; | |
413 | (void)sprintf(buf, | |
414 | "%d%c %6d %-11s (%d on %d)", | |
415 | item + offset, sp->hs_time ? '*' : ' ', | |
416 | sp->hs_score * sp->hs_level, | |
417 | sp->hs_name, sp->hs_score, sp->hs_level); | |
418 | /* | |
419 | * Highlight if appropriate. This works because | |
420 | * we only get one score per level. | |
421 | */ | |
422 | if (me != NULL && | |
423 | sp->hs_level == level && | |
424 | sp->hs_score == score && | |
425 | strcmp(sp->hs_name, me) == 0) { | |
426 | putpad(SOstr); | |
427 | highlight = 1; | |
428 | } | |
429 | (void)printf("%s", buf); | |
430 | if (highlight) { | |
431 | putpad(SEstr); | |
432 | highlight = 0; | |
433 | } | |
434 | ||
435 | /* fill in spaces so column 1 lines up */ | |
436 | if (col == 0) | |
437 | for (i = 40 - strlen(buf); --i >= 0;) | |
438 | (void)putchar(' '); | |
439 | else /* col == 1 */ | |
440 | (void)putchar('\n'); | |
441 | } | |
442 | } | |
443 | } |