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