1 /* Management of historical information about an Xconq game.
2    Copyright (C) 1992-2000 Stanley T. Shebs.
3 
4 Xconq is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2, or (at your option)
7 any later version.  See the file COPYING.  */
8 
9 #include "conq.h"
10 
11 static void notify_event(Side *side, HistEvent *hevt);
12 static void play_event_movies(Side *side, HistEvent *hevt);
13 static void rewrite_unit_references(int oldid, int newid);
14 static void rewrite_unit_references_in_event(HistEvent *hevt, int oldid,
15 					     int newid);
16 
17 extern int any_post_event_scores;
18 extern int need_post_event_scores;
19 
20 HevtDefn hevtdefns[] = {
21 
22 #undef  DEF_HEVT
23 #define DEF_HEVT(NAME, code, DATADESCS) { NAME, DATADESCS },
24 
25 #include "history.def"
26 
27     { NULL, NULL }
28 };
29 
30 /* Head of the list of events. */
31 
32 HistEvent *history;
33 
34 int tmphevtdata1;
35 
36 /* The list of all past unit records. */
37 
38 PastUnit *past_unit_list;
39 
40 PastUnit *last_past_unit;
41 
42 /* The id of the next past unit to be synthesized.  -1 is reserved, so
43    start counting down from -2. */
44 
45 int next_past_unit_id = -2;
46 
47 /* Buffers for descriptions of past units. */
48 
49 #define NUMPASTBUFS 3
50 
51 int curpastbuf = 0;
52 
53 char *pastbufs[NUMPASTBUFS] = { NULL, NULL, NULL };
54 
55 /* True if events are being recorded into the history. */
56 
57 static int recording_events;
58 
59 /* True if statistics dump is wanted. */
60 
61 int statistics_wanted = FALSE;
62 
63 /* True after the statistics file has been written. */
64 
65 int statistics_dumped;
66 
67 void
init_history(void)68 init_history(void)
69 {
70     /* The first "event" is just a marker. */
71     history = create_historical_event(H_LOG_HEAD);
72     /* Give it an impossible date. */
73     history->startdate = -1;
74     history->next = history->prev = history;
75     /* Initialize the past unit record. */
76     past_unit_list = last_past_unit = NULL;
77     next_past_unit_id = -2;
78 }
79 
80 void
start_history(void)81 start_history(void)
82 {
83     /* Ignore multiple starts. */
84     if (recording_events)
85       return;
86     recording_events = TRUE;
87     record_event(H_LOG_STARTED, ALLSIDES);
88 }
89 
90 HistEvent *
create_historical_event(HistEventType type)91 create_historical_event(HistEventType type)
92 {
93     HistEvent *hevt = (HistEvent *) xmalloc(sizeof(HistEvent));
94 
95     if (type >= NUMHEVTTYPES)
96       run_warning("unknown hist event type %d", type);
97     hevt->type = type;
98     hevt->observers = ALLSIDES;
99     hevt->next = hevt->prev = NULL;
100     return hevt;
101 }
102 
103 /* Record a historical event. */
104 
105 HistEvent *
record_event(HistEventType type,SideMask observers,...)106 record_event(HistEventType type, SideMask observers, ...)
107 {
108     int i, val;
109     char *descs;
110     HistEvent *hevt;
111     Side *side;
112     va_list ap;
113 
114     if (!recording_events)
115       return NULL;
116     hevt = create_historical_event(type);
117     hevt->startdate = g_turn();
118     hevt->enddate = g_turn();
119     hevt->observers = observers;
120     descs = hevtdefns[type].datadescs;
121 
122     va_start(ap, observers);
123     for (i = 0; descs[i] != '\0'; ++i) {
124 	if (i >= 4)
125 	  run_error("hevt type %d has too many parameters", type);
126 	val = va_arg(ap, int);
127 	hevt->data[i] = val;
128     }
129     va_end(ap);
130 
131     /* Check on plausibility of event data. */
132     for (i = 0; descs[i] != '\0'; ++i) {
133 	val = hevt->data[i];
134 	switch (descs[i]) {
135 	  case 'S':
136 	    if (!between(0, val, numsides))
137 	      run_warning("invalid side number %d in hist event", val);
138 	    break;
139 	  case 'U':
140 	    /* Note that when validating unit id, the unit may be in
141 	       the middle of the the process of becoming a past unit,
142 	       so allowing finding a dead unit. */
143 	    if (val != 0
144 		&& find_unit_dead_or_alive(val) == NULL
145 		&& find_past_unit(val) == NULL)
146 	      run_warning("invalid unit/pastunit id %d in hist event", val);
147 	    break;
148 	  default:
149 	    break;
150 	}
151     }
152     /* Insert the newly created event. */
153     hevt->next = history;
154     hevt->prev = history->prev;
155     history->prev->next = hevt;
156     history->prev = hevt;
157     Dprintf("Recorded event %s (observed by %d)\n",
158 	    hevtdefns[hevt->type].name, hevt->observers);
159     if (observers != NOSIDES) {
160 	/* Let all the observers' interfaces look at this event. */
161 	for_all_sides(side) {
162 	    if (side_in_set(side, observers) && active_display(side)) {
163 		update_event_display(side, hevt, TRUE);
164 		notify_event(side, hevt);
165 		if (g_event_movies() != lispnil) {
166 		    play_event_movies(side, hevt);
167 		}
168 	    }
169 	}
170     }
171     /* Flag that we should look at post-event scorekeepers soon. */
172     if (any_post_event_scores && !beforestart && !endofgame)
173       need_post_event_scores = TRUE;
174     return hevt;
175 }
176 
177 static void
notify_event(Side * side,HistEvent * hevt)178 notify_event(Side *side, HistEvent *hevt)
179 {
180     char abuf[BUFSIZE];
181     Obj *rest, *head, *pattern, *text;
182     Side *side2;
183     Unit *unit;
184 
185     for_all_list(g_event_notices(), rest) {
186 	head = car(rest);
187 	if (consp(head)) {
188 	    pattern = car(head);
189 	    if (symbolp(pattern)
190 		&& find_event_type(pattern) == hevt->type) {
191 		text = cadr(head);
192 		if (stringp(text)) {
193 		    sprintf(abuf, "%s", c_string(text));
194 		} else {
195 		    sprintlisp(abuf, text, 50);
196 		}
197 		notify(side, "%s", abuf);
198 		return;
199 	    } else if (consp(pattern)
200 		       && symbolp(car(pattern))
201 		       && pattern_matches_event(pattern, hevt)
202 		       ) {
203 		text = cadr(head);
204 		if (stringp(text)) {
205 		    strcpy(abuf, c_string(text));
206 		} else {
207 		    event_desc_from_list(side, text, hevt, abuf);
208 		}
209 		notify(side, "%s", abuf);
210 		return;
211 	    }
212 	}
213     }
214     /* If we didn't find anything special, still put out some generic
215        messages for important cases. */
216     switch (hevt->type) {
217       case H_SIDE_LOST:
218 	if (hevt->data[0] == side_number(side)) {
219 	    notify(side, "You lost!");
220 	} else {
221 	    notify(side, "%s lost!", side_desig(side_n(hevt->data[0])));
222 	}
223 	break;
224       case H_SIDE_WON:
225 	if (hevt->data[0] == side_number(side)) {
226 	    notify(side, "You won!");
227 	} else {
228 	    notify(side, "%s won!", side_desig(side_n(hevt->data[0])));
229 	}
230 	break;
231 #if 0 /* this already has a special message */
232       case H_GAME_ENDED:
233 	notify(side, "The game is over!");
234 	break;
235 #endif
236       case H_UNIT_COMPLETED:
237 	side2 = side_n(hevt->data[0]);
238 	unit = find_unit(hevt->data[1]);
239 	if (unit != NULL) {
240 	    if (side2 == side) {
241 		notify(side, "You completed %s.",
242 		       unit_handle(side, unit));
243 	    } else {
244 		notify(side, "%s completed %s.",
245 		       side_desig(side2), unit_handle(side2, unit));
246 	    }
247 	}
248 	break;
249       case H_UNIT_CREATED:
250 	side2 = side_n(hevt->data[0]);
251 	unit = find_unit(hevt->data[1]);
252 	if (unit != NULL) {
253 	    if (side2 == side) {
254 		notify(side, "You created %s.",
255 		       unit_handle(side, unit));
256 	    } else {
257 		notify(side, "%s created %s.",
258 		       side_desig(side2), unit_handle(side2, unit));
259 	    }
260 	}
261 	break;
262       default:
263 	break;
264     }
265 }
266 
267 static void
play_event_movies(Side * side,HistEvent * hevt)268 play_event_movies(Side *side, HistEvent *hevt)
269 {
270     int found = FALSE;
271     char *soundname;
272     Obj *rest, *head, *parms, *msgdesc;
273 
274     if (!should_play_movies())
275       return;
276     for_all_list(g_event_movies(), rest) {
277 	head = car(rest);
278 	if (consp(head)
279 	    && symbolp(car(head))
280 	    && hevt->type == find_event_type(car(head))) {
281 	    found = TRUE;
282 	    break;
283 	}
284 	if (consp(head)
285 	    && consp(car(head))
286 	    && symbolp(car(car(head)))
287 	    && hevt->type == find_event_type(car(car(head)))) {
288 	    parms = cdr(car(head));
289 	    if (parms == lispnil) {
290 		found = TRUE;
291 		break;
292 	    }
293 #if 0
294 	    if (((symbolp(car(parms))
295 		   && strcmp(c_string(car(parms)),
296 			     u_type_name(unit->type)) == 0)
297 		  || match_keyword(car(parms), K_USTAR)
298 		  || (symbolp(car(parms))
299 		      && boundp(car(parms))
300 		      && ((symbolp(symbol_value(car(parms)))
301 		          && strcmp(c_string(symbol_value(car(parms))),
302 				    u_type_name(unit->type)) == 0)
303 		          || (numberp(symbol_value(car(parms)))
304 		              && c_number(symbol_value(car(parms)))
305 			      == unit->type)))
306 		  )) {
307 		found = TRUE;
308 		break;
309 	    }
310 	    /* (should be able to match on particular data also) */
311 #endif
312 	}
313     }
314     /* If we have a match, do something with it. */
315     if (found) {
316 	msgdesc = cadr(head);
317 	if (stringp(msgdesc)) {
318 	    notify(side, "%s", c_string(msgdesc));
319 	} else if (consp(msgdesc)
320 		   && symbolp(car(msgdesc))
321 		   && strcmp(c_string(car(msgdesc)), "sound") == 0
322 		   && stringp(cadr(msgdesc))) {
323 	    soundname = c_string(cadr(msgdesc));
324 	    /* (should not be passing ptrs to schedule_movie) */
325 	    schedule_movie(side, "sound", soundname);
326 	    play_movies(add_side_to_set(side, NOSIDES));
327 	} else {
328 	}
329     }
330 }
331 
332 /* A unit's death requires some bookkeeping - we need to update the
333    history to use a past unit, plus record the loss for statistics
334    purposes. */
335 
336 void
record_unit_death(Unit * unit,HistEventType reason)337 record_unit_death(Unit *unit, HistEventType reason)
338 {
339     enum loss_reasons lossreason;
340     PastUnit *pastunit;
341 
342     pastunit = create_past_unit(unit->type, 0);
343     pastunit->name = unit->name;
344     pastunit->number = unit->number;
345     pastunit->x = unit->x;  pastunit->y = unit->y;  pastunit->z = unit->z;
346     pastunit->side = unit->side;
347     rewrite_unit_references(unit->id, pastunit->id);
348     if (reason >= 0) {
349 	switch (reason) {
350 	  case H_UNIT_GARRISONED:
351 	    /* Garrison events mention the unit being garrisoned. */
352 	    record_event(reason, add_side_to_set(unit->side, NOSIDES),
353 			 pastunit->id, tmphevtdata1);
354 	    break;
355 	  default:
356 	    record_event(reason, add_side_to_set(unit->side, NOSIDES),
357 			 pastunit->id, -1);
358 	    break;
359 	}
360     }
361     /* Reduce specific events to general categories of unit loss, used
362        for statistics display. */
363     /* Note that we track independent unit losses as well. */
364     switch (reason) {
365       case H_UNIT_KILLED:
366       case H_UNIT_WRECKED:
367 	lossreason = combat_loss;
368 	break;
369       case H_UNIT_STARVED:
370 	lossreason = starvation_loss;
371 	break;
372       case H_UNIT_DIED_IN_ACCIDENT:
373       case H_UNIT_WRECKED_IN_ACCIDENT:
374       case H_UNIT_VANISHED:
375 	lossreason = accident_loss;
376 	break;
377       case H_UNIT_DIED_FROM_ATTRITION:
378       case H_UNIT_WRECKED_FROM_ATTRITION:
379 	lossreason = attrition_loss;
380 	break;
381       case H_UNIT_DISBANDED:
382 	lossreason = disband_loss;
383 	break;
384       default:
385 	lossreason = other_loss;
386 	break;
387     }
388     count_loss(unit->side, unit->type, lossreason);
389 }
390 
391 /* Create a past unit corresponding to the given unit, replace unit
392    references in the history. */
393 
394 PastUnit *
change_unit_to_past_unit(Unit * unit)395 change_unit_to_past_unit(Unit *unit)
396 {
397     PastUnit *pastunit;
398 
399     pastunit = create_past_unit(unit->type, 0);
400     pastunit->name = unit->name;
401     pastunit->number = unit->number;
402     pastunit->x = unit->x;  pastunit->y = unit->y;  pastunit->z = unit->z;
403     pastunit->side = unit->side;
404     rewrite_unit_references(unit->id, pastunit->id);
405     return pastunit;
406 }
407 
408 void
record_unit_side_change(Unit * unit,Side * newside,HistEventType reason,Unit * agent)409 record_unit_side_change(Unit *unit, Side *newside, HistEventType reason,
410 			Unit *agent)
411 {
412     int capture;
413     enum loss_reasons lossreason;
414     enum gain_reasons gainreason;
415     SideMask observers;
416     PastUnit *pastunit;
417 
418     /* Side loss hevt has no space for individual units, so change to a type
419        of event that does. */
420     if (reason == H_SIDE_LOST)
421       reason = H_UNIT_ACQUIRED;
422     pastunit = change_unit_to_past_unit(unit);
423     observers = NOSIDES;
424     observers = add_side_to_set(unit->side, observers);
425     observers = add_side_to_set(newside, observers);
426     if (agent != NULL)
427       observers = add_side_to_set(agent->side, observers);
428     /* Check the event type to decide how many parameters to pass to
429        the generic recorder. */
430     capture = (reason == H_UNIT_CAPTURED || reason == H_UNIT_SURRENDERED);
431     if (capture)
432       record_event(reason, observers, pastunit->id, (agent ? agent->id : 0),
433 		   side_number(newside));
434     else
435       record_event(reason, observers, pastunit->id,
436 		   side_number(newside));
437     lossreason = (capture ? capture_loss : other_loss);
438     count_loss(unit->side, unit->type, lossreason);
439     gainreason = (capture ? capture_gain : other_gain);
440     count_gain(newside, unit->type, gainreason);
441 }
442 
443 /* Record a unit's name change by creating a past unit with the
444    previous name. */
445 
446 void
record_unit_name_change(Unit * unit,char * newname)447 record_unit_name_change(Unit *unit, char *newname)
448 {
449     PastUnit *pastunit;
450 
451     pastunit = change_unit_to_past_unit(unit);
452     record_event(H_UNIT_NAME_CHANGED, ALLSIDES, pastunit->id, unit->id);
453 }
454 
455 void
count_gain(Side * side,int u,enum gain_reasons reason)456 count_gain(Side *side, int u, enum gain_reasons reason)
457 {
458     assert_error((reason < num_gain_reasons),
459 		 "Invalid gain reason given for historical record.");
460     assert_error(is_unit_type(u),
461 		 "Invalid unit type given for historical record.");
462     if (side)
463       ++(side->gaincounts[num_gain_reasons * u + reason]);
464 }
465 
466 void
count_loss(Side * side,int u,enum loss_reasons reason)467 count_loss(Side *side, int u, enum loss_reasons reason)
468 {
469     assert_error((reason < num_loss_reasons),
470 		 "Invalid loss reason given for historical record.");
471     assert_error(is_unit_type(u),
472 		 "Invalid unit type given for historical record.");
473     if (side)
474       ++(side->losscounts[num_loss_reasons * u + reason]);
475 }
476 
477 /* Create a past unit with the given type and for the unit with
478    the given id. */
479 
480 PastUnit *
create_past_unit(int type,int id)481 create_past_unit(int type, int id)
482 {
483     PastUnit *pastunit = (PastUnit *) xmalloc(sizeof(PastUnit));
484 
485     pastunit->type = type;
486     if (id == 0)
487       id = next_past_unit_id--;
488     else
489       next_past_unit_id = min(next_past_unit_id, id - 1);
490     pastunit->id = id;
491     /* Glue at the end of the list, so all stays sorted. */
492     if (past_unit_list == NULL) {
493 	past_unit_list = last_past_unit = pastunit;
494     } else {
495 	last_past_unit->next = pastunit;
496 	last_past_unit = pastunit;
497     }
498     return pastunit;
499 }
500 
501 PastUnit *
find_past_unit(int n)502 find_past_unit(int n)
503 {
504     PastUnit *pastunit;
505 
506     for (pastunit = past_unit_list; pastunit != NULL; pastunit = pastunit->next) {
507 	if (pastunit->id == n)
508 	  return pastunit;
509     }
510     return NULL;
511 }
512 
513 /* Scan through the history, changing matching unit/pastunit
514    references into past unit references. */
515 
516 static void
rewrite_unit_references(int oldid,int newid)517 rewrite_unit_references(int oldid, int newid)
518 {
519     HistEvent *hevt;
520 
521     rewrite_unit_references_in_event(history, oldid, newid);
522     for (hevt = history->next; hevt != history; hevt = hevt->next) {
523 	rewrite_unit_references_in_event(hevt, oldid, newid);
524     }
525 }
526 
527 static void
rewrite_unit_references_in_event(HistEvent * hevt,int oldid,int newid)528 rewrite_unit_references_in_event(HistEvent *hevt, int oldid, int newid)
529 {
530     int i;
531     char *descs;
532 
533     descs = hevtdefns[hevt->type].datadescs;
534     /* Scan through the data description, looking for values that are
535        references to actual units. */
536     for (i = 0; descs[i] != '\0'; ++i) {
537 	if (descs[i] == 'U' && hevt->data[i] == oldid)
538 	  hevt->data[i] = newid;
539     }
540 }
541 
542 /* Indicate that history is no longer being recorded. */
543 
544 void
end_history(void)545 end_history(void)
546 {
547     record_event(H_LOG_ENDED, ALLSIDES);
548     recording_events = FALSE;
549 }
550 
551 /* Find the historical event that would be at the given index, in
552    a list where each event gets one line, and dates appear on
553    separate lines whenever the date is different. */
554 
555 HistEvent *
get_nth_history_line(Side * side,int n,HistEvent ** nextevt)556 get_nth_history_line(Side *side, int n, HistEvent **nextevt)
557 {
558     int i = 0;
559     HistEvent *hevt, *hevtprev = NULL;
560     char buf[500];
561 
562     Dprintf("Getting line %d\n", n);
563     for (hevt = history->next; hevt != history; hevt = hevt->next) {
564 	historical_event_desc(side, hevt, buf);
565 	Dprintf("  event is %s, date is %d, i is %d\n", buf, hevt->startdate, i);
566 	if (side_in_set(side, hevt->observers)) {
567 	    Dprintf("    observed\n");
568 	    if (hevtprev == NULL || hevt->startdate != hevtprev->startdate) {
569 		if (i == n) {
570 		    Dprintf("Returning NULL\n");
571 		    return NULL;
572 		}
573 		++i;
574 	    }
575 	    if (i == n) {
576 		historical_event_desc(side, hevt, buf);
577 		    Dprintf("Returning %s\n", hevt);
578 	      return hevt;
579 	    }
580 	    ++i;
581 	    hevtprev = hevt;
582 	}
583     }
584     /* Return the last event. */
585     return history->prev;
586 }
587 
588 /* Count the total number of lines that can be in the history. */
589 
590 int
update_total_hist_lines(Side * side)591 update_total_hist_lines(Side *side)
592 {
593     int nlines;
594     HistEvent *hevt, *hevtprev = NULL;
595 
596     nlines = 0;
597     for (hevt = history->next; hevt != history; hevt = hevt->next) {
598 	if (side_in_set(side, hevt->observers)) {
599 	    /* Count an extra line for date changes. */
600 	    if (hevtprev == NULL || hevt->startdate != hevtprev->startdate)
601 	      ++nlines;
602 	    ++nlines;
603 	    hevtprev = hevt;
604 	}
605     }
606     return nlines;
607 }
608 
609 /* This routine builds up the array of events and dates to draw, with
610    a combination of pointers to hevts and NULLs where dates should
611    appear. */
612 
613 int
build_hist_contents(Side * side,int n,HistEvent ** histcontents,int numvishistlines)614 build_hist_contents(Side *side, int n, HistEvent **histcontents,
615 		    int numvishistlines)
616 {
617     int numcontents;
618     HistEvent *hevt, *nexthevt;
619 
620     numcontents = 0;
621     hevt = get_nth_history_line(side, n, &nexthevt);
622     histcontents[numcontents++] = hevt;
623     /* Since we want to iterate over links (get_nth_history_line may not
624        be very efficient), ensure that we have an actual hevt. */
625     if (hevt == NULL) {
626 	hevt = get_nth_history_line(side, n + 1, &nexthevt);
627 	histcontents[numcontents++] = hevt;
628     }
629     for (hevt = hevt->next; hevt != history; hevt = hevt->next) {
630 	/* Stop if we've filled the contents buffer. */
631 	if (numcontents >= numvishistlines)
632 	  break;
633 	if (side_in_set(side, hevt->observers)) {
634 	    /* Leave a NULL if the date of this event is different
635                from the last. */
636 	    if (numcontents > 0
637 		&& histcontents[numcontents - 1] != NULL
638 		&& hevt->startdate != histcontents[numcontents - 1]->startdate) {
639 		histcontents[numcontents++] = NULL;
640 	    }
641 	    histcontents[numcontents++] = hevt;
642 	}
643     }
644     return numcontents;
645 }
646 
647 /* Summarize various aspects of performance in the game, writing it
648    all to a file. */
649 
650 void
dump_statistics(void)651 dump_statistics(void)
652 {
653     char *fname;
654     Side *side;
655     FILE *fp;
656 
657     if (!statistics_wanted)
658       return;
659     fname = statistics_filename();
660     /* If no name returned, assume that there is a good reason
661        (for instance, the user might have cancelled). */
662     if (empty_string(fname))
663       return;
664     fp = open_file(fname, "w");
665     if (fp != NULL) {
666 	if (1 /* records exist */) {
667 	    write_side_results(fp, NULL);
668 	    write_unit_record(fp, NULL);
669 	    write_combat_results(fp, NULL);
670 	    fprintf(fp, "\f\n");
671 	    for_all_sides(side) {
672 		write_side_results(fp, side);
673 		write_unit_record(fp, side);
674 		write_combat_results(fp, side);
675 		if (side->next != NULL)
676 		  fprintf(fp, "\f\n");
677 	    }
678 	} else {
679 	    fprintf(fp, "No statistics were kept.\n");
680 	}
681 	statistics_dumped = TRUE;
682 	fclose(fp);
683     } else {
684 	run_warning("Can't open statistics file \"%s\"", fname);
685     }
686 }
687 
688 /* Short, unreadable, but greppable listing of past unit.  Primarily
689    useful for debugging and warnings.  We use several buffers and
690    rotate between them so we can call this more than once in a single
691    printf. */
692 
693 char *
past_unit_desig(PastUnit * pastunit)694 past_unit_desig(PastUnit *pastunit)
695 {
696     char *shortbuf;
697 
698     if (pastunit == NULL)
699       return "no pastunit";
700     /* Allocate if not yet done so. */
701     if (pastbufs[curpastbuf] == NULL)
702       pastbufs[curpastbuf] = (char *)xmalloc(BUFSIZE);
703     shortbuf = pastbufs[curpastbuf];
704     curpastbuf = (curpastbuf + 1) % NUMPASTBUFS;
705     if (pastunit->id == -1) {
706 	sprintf(shortbuf, "s%d head", side_number(pastunit->side));
707 	return shortbuf;
708     } else if (is_unit_type(pastunit->type)) {
709 	sprintf(shortbuf, "s%d %-3.3s %d (%d,%d",
710 		side_number(pastunit->side),
711 		shortest_unique_name(pastunit->type),
712 		pastunit->id, pastunit->x, pastunit->y);
713 	if (pastunit->z != 0)
714 	  tprintf(shortbuf, ",%d", pastunit->z);
715 	strcat(shortbuf, ")");  /* close out the pastunit location */
716 	return shortbuf;
717     } else {
718 	return "!garbage pastunit!";
719     }
720 }
721 
722 /* Would be faster to stash these, but enough difference to care? */
723 
724 int
total_gain(Side * side,int u)725 total_gain(Side *side, int u)
726 {
727     int i, total = 0;
728 
729     for (i = 0; i < num_gain_reasons; ++i)
730       total += side_gain_count(side, u, i);
731     return total;
732 }
733 
734 int
total_loss(Side * side,int u)735 total_loss(Side *side, int u)
736 {
737     int i, total = 0;
738 
739     for (i = 0; i < num_loss_reasons; ++i)
740       total += side_loss_count(side, u, i);
741     return total;
742 }
743