scores.c revision 1.13
1/* $NetBSD: scores.c,v 1.13 2004/01/27 20:30:30 jsm Exp $ */ 2 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 */ 36 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 <termcap.h> 54#include <unistd.h> 55 56#include "pathnames.h" 57#include "screen.h" 58#include "scores.h" 59#include "tetris.h" 60 61/* 62 * Within this code, we can hang onto one extra "high score", leaving 63 * room for our current score (whether or not it is high). 64 * 65 * We also sometimes keep tabs on the "highest" score on each level. 66 * As long as the scores are kept sorted, this is simply the first one at 67 * that level. 68 */ 69#define NUMSPOTS (MAXHISCORES + 1) 70#define NLEVELS (MAXLEVEL + 1) 71 72static time_t now; 73static int nscores; 74static int gotscores; 75static struct highscore scores[NUMSPOTS]; 76 77static int checkscores(struct highscore *, int); 78static int cmpscores(const void *, const void *); 79static void getscores(FILE **); 80static void printem(int, int, struct highscore *, int, const char *); 81static char *thisuser(void); 82 83/* 84 * Read the score file. Can be called from savescore (before showscores) 85 * or showscores (if savescore will not be called). If the given pointer 86 * is not NULL, sets *fpp to an open file pointer that corresponds to a 87 * read/write score file that is locked with LOCK_EX. Otherwise, the 88 * file is locked with LOCK_SH for the read and closed before return. 89 * 90 * Note, we assume closing the stdio file releases the lock. 91 */ 92static void 93getscores(fpp) 94 FILE **fpp; 95{ 96 int sd, mint, lck; 97 mode_t mask; 98 const char *mstr, *human; 99 FILE *sf; 100 101 if (fpp != NULL) { 102 mint = O_RDWR | O_CREAT; 103 mstr = "r+"; 104 human = "read/write"; 105 lck = LOCK_EX; 106 } else { 107 mint = O_RDONLY; 108 mstr = "r"; 109 human = "reading"; 110 lck = LOCK_SH; 111 } 112 setegid(egid); 113 mask = umask(S_IWOTH); 114 sd = open(_PATH_SCOREFILE, mint, 0666); 115 (void)umask(mask); 116 if (sd < 0) { 117 if (fpp == NULL) { 118 nscores = 0; 119 setegid(gid); 120 return; 121 } 122 err(1, "cannot open %s for %s", _PATH_SCOREFILE, human); 123 } 124 if ((sf = fdopen(sd, mstr)) == NULL) { 125 err(1, "cannot fdopen %s for %s", _PATH_SCOREFILE, human); 126 } 127 setegid(gid); 128 129 /* 130 * Grab a lock. 131 */ 132 if (flock(sd, lck)) 133 warn("warning: score file %s cannot be locked", 134 _PATH_SCOREFILE); 135 136 nscores = fread(scores, sizeof(scores[0]), MAXHISCORES, sf); 137 if (ferror(sf)) { 138 err(1, "error reading %s", _PATH_SCOREFILE); 139 } 140 141 if (fpp) 142 *fpp = sf; 143 else 144 (void)fclose(sf); 145} 146 147void 148savescore(level) 149 int level; 150{ 151 struct highscore *sp; 152 int i; 153 int change; 154 FILE *sf; 155 const char *me; 156 157 getscores(&sf); 158 gotscores = 1; 159 (void)time(&now); 160 161 /* 162 * Allow at most one score per person per level -- see if we 163 * can replace an existing score, or (easiest) do nothing. 164 * Otherwise add new score at end (there is always room). 165 */ 166 change = 0; 167 me = thisuser(); 168 for (i = 0, sp = &scores[0]; i < nscores; i++, sp++) { 169 if (sp->hs_level != level || strcmp(sp->hs_name, me) != 0) 170 continue; 171 if (score > sp->hs_score) { 172 (void)printf("%s bettered %s %d score of %d!\n", 173 "\nYou", "your old level", level, 174 sp->hs_score * sp->hs_level); 175 sp->hs_score = score; /* new score */ 176 sp->hs_time = now; /* and time */ 177 change = 1; 178 } else if (score == sp->hs_score) { 179 (void)printf("%s tied %s %d high score.\n", 180 "\nYou", "your old level", level); 181 sp->hs_time = now; /* renew it */ 182 change = 1; /* gotta rewrite, sigh */ 183 } /* else new score < old score: do nothing */ 184 break; 185 } 186 if (i >= nscores) { 187 strcpy(sp->hs_name, me); 188 sp->hs_level = level; 189 sp->hs_score = score; 190 sp->hs_time = now; 191 nscores++; 192 change = 1; 193 } 194 195 if (change) { 196 /* 197 * Sort & clean the scores, then rewrite. 198 */ 199 nscores = checkscores(scores, nscores); 200 rewind(sf); 201 if (fwrite(scores, sizeof(*sp), nscores, sf) != (size_t)nscores || 202 fflush(sf) == EOF) 203 warnx("error writing %s: %s -- %s", 204 _PATH_SCOREFILE, strerror(errno), 205 "high scores may be damaged"); 206 } 207 (void)fclose(sf); /* releases lock */ 208} 209 210/* 211 * Get login name, or if that fails, get something suitable. 212 * The result is always trimmed to fit in a score. 213 */ 214static char * 215thisuser() 216{ 217 const char *p; 218 struct passwd *pw; 219 size_t l; 220 static char u[sizeof(scores[0].hs_name)]; 221 222 if (u[0]) 223 return (u); 224 p = getlogin(); 225 if (p == NULL || *p == '\0') { 226 pw = getpwuid(getuid()); 227 if (pw != NULL) 228 p = pw->pw_name; 229 else 230 p = " ???"; 231 } 232 l = strlen(p); 233 if (l >= sizeof(u)) 234 l = sizeof(u) - 1; 235 memcpy(u, p, l); 236 u[l] = '\0'; 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 */ 246static int 247cmpscores(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 */ 276static int 277checkscores(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 */ 350void 351showscores(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\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 = 40; 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 403static void 404printem(level, offset, hs, n, me) 405 int level, offset; 406 struct highscore *hs; 407 int n; 408 const char *me; 409{ 410 struct highscore *sp; 411 int nrows, row, col, item, i, highlight; 412 char buf[100]; 413#define TITLE "Rank Score Name (points/level)" 414 415 /* 416 * This makes a nice two-column sort with headers, but it's a bit 417 * convoluted... 418 */ 419 printf("%s %s\n", TITLE, n > 1 ? TITLE : ""); 420 421 highlight = 0; 422 nrows = (n + 1) / 2; 423 424 for (row = 0; row < nrows; row++) { 425 for (col = 0; col < 2; col++) { 426 item = col * nrows + row; 427 if (item >= n) { 428 /* 429 * Can only occur on trailing columns. 430 */ 431 (void)putchar('\n'); 432 continue; 433 } 434 sp = &hs[item]; 435 (void)sprintf(buf, 436 "%3d%c %6d %-11s (%6d on %d)", 437 item + offset, sp->hs_time ? '*' : ' ', 438 sp->hs_score * sp->hs_level, 439 sp->hs_name, sp->hs_score, sp->hs_level); 440 /* 441 * Highlight if appropriate. This works because 442 * we only get one score per level. 443 */ 444 if (me != NULL && 445 sp->hs_level == level && 446 sp->hs_score == score && 447 strcmp(sp->hs_name, me) == 0) { 448 putpad(SOstr); 449 highlight = 1; 450 } 451 (void)printf("%s", buf); 452 if (highlight) { 453 putpad(SEstr); 454 highlight = 0; 455 } 456 457 /* fill in spaces so column 1 lines up */ 458 if (col == 0) 459 for (i = 40 - strlen(buf); --i >= 0;) 460 (void)putchar(' '); 461 else /* col == 1 */ 462 (void)putchar('\n'); 463 } 464 } 465} 466