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