]> git.cworth.org Git - mnemon/blob - mnemon.c
Generalize histogram printing to accept a predicate function
[mnemon] / mnemon.c
1 /*
2  * Copyright © 2006 Carl Worth
3  *
4  * This program 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.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software Foundation,
16  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."
17  */
18
19 /* for asprintf */
20 #define _GNU_SOURCE
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <stdarg.h>
24 #include <stdint.h>
25 #include <math.h>
26
27 #include <sys/types.h>
28 #include <dirent.h>
29 #include <errno.h>
30 #include <string.h>
31 #include <assert.h>
32
33 #include <readline/readline.h>
34 #include <readline/history.h>
35
36 typedef int bool_t;
37
38 typedef struct _item {
39     int score;
40     char *challenge;
41     char *response;
42 } item_t;
43
44 typedef struct _bin {
45     int score;
46     int items_size;
47     int num_items;
48     item_t **items;
49 } bin_t;
50
51 typedef enum {
52     CATEGORY_ORDER_RANDOM,
53     CATEGORY_ORDER_SEQUENTIAL
54 } category_order_t;
55
56 typedef struct _category {
57     char *name;
58     int items_size;
59     int num_items;
60     item_t *items;
61
62     /* Support sequential introduction of items from bin 0 */
63     category_order_t order;
64     int bin_zero_head;
65 } category_t;
66
67 typedef struct _mnemon {
68     char *dir_name;
69
70     int categories_size;
71     int num_categories;
72     category_t *categories;
73
74     int bins_size;
75     int num_bins;
76     bin_t *bins;
77
78     int to_introduce;
79     int to_master;
80     int unlearned;
81     int mastered;
82 } mnemon_t;
83
84 static void *
85 xmalloc (size_t size)
86 {
87     void *ret;
88
89     ret = malloc (size);
90     if (ret == NULL) {
91         fprintf (stderr, "Error: out of memory\n");
92         exit (1);
93     }
94
95     return ret;
96 }
97
98 static void *
99 xrealloc (void *ptr, size_t size)
100 {
101     void *ret;
102
103     ret = realloc (ptr, size);
104     if (ret == NULL) {
105         fprintf (stderr, "Error: out of memory\n");
106         exit (1);
107     }
108
109     return ret;
110 }
111
112 static char *
113 xstrdup (const char *s)
114 {
115     char *ret;
116
117     ret = strdup (s);
118     if (s == NULL) {
119         fprintf (stderr, "Error: out of memory\n");
120         exit (1);
121     }
122
123     return ret;
124 }
125
126 static void
127 xasprintf (char **strp, const char *fmt, ...)
128 {
129     va_list ap;
130     int ret;
131
132     va_start (ap, fmt);
133     ret = vasprintf (strp, fmt, ap);
134     va_end (ap);
135
136     if (ret < 0) {
137         fprintf (stderr, "Error: out of memory\n");
138         exit (1);
139     }
140 }
141
142 static void
143 item_init (item_t       *item,
144            int           score,
145            const char   *challenge,
146            const char   *response)
147 {
148     item->score = score;
149
150     item->challenge = xmalloc (strlen (challenge) + 1 +
151                                strlen (response) + 1);
152     item->response = item->challenge + strlen (challenge) + 1;
153
154     strcpy (item->challenge, challenge);
155     strcpy (item->response, response);
156 }
157
158 static void
159 item_fini (item_t *item)
160 {
161     /* item->response shares allocation with item->challenge, so
162      * doesn't require a separate call to free */
163     free (item->challenge);
164 }
165
166 static void
167 category_init (category_t *category,
168                const char *name)
169 {
170     category->name = xstrdup (name);
171     
172     category->items_size = 0;
173     category->num_items = 0;
174     category->items = NULL;
175     category->order = CATEGORY_ORDER_RANDOM;
176     category->bin_zero_head = 0;
177 }
178
179 static void
180 category_fini (category_t *category)
181 {
182     int i;
183
184     for (i = 0; i < category->num_items; i++)
185         item_fini (&category->items[i]);
186
187     free (category->items);
188
189     free (category->name);
190 }
191
192 static void
193 category_grow (category_t *category)
194 {
195     if (category->items_size)
196         category->items_size *= 2;
197     else
198         category->items_size = 1;
199
200     category->items = xrealloc (category->items,
201                                 category->items_size * sizeof (item_t));
202 }
203
204 static item_t *
205 category_add_item (category_t   *category,
206                    int           score,
207                    const char   *challenge,
208                    const char   *response)
209 {
210     item_t *item;
211
212     if (category->num_items == category->items_size)
213         category_grow (category);
214
215     item = &category->items[category->num_items++];
216
217     item_init (item, score, challenge, response);
218
219     return item;
220 }
221
222 static item_t *
223 category_next_bin_zero_item (category_t *category)
224 {
225     int *i = &category->bin_zero_head;
226
227     for ( ; *i < category->num_items; *i = *i + 1)
228         if (category->items[*i].score == 0)
229             return &category->items[*i];
230
231     return NULL;
232 }
233
234 static void
235 category_print (category_t      *category,
236                 FILE            *file)
237 {
238     int i;
239     item_t *item;
240
241     fprintf (file, "order = %s\n\n",
242             category->order == CATEGORY_ORDER_RANDOM ? "random" : "sequential");
243
244     for (i = 0; i < category->num_items; i++) {
245         item = &category->items[i];
246         if (i != 0)
247             fprintf (file, "\n");
248         fprintf (file, "%d\n%s\n%s\n",
249                  item->score,
250                  item->challenge,
251                  item->response);
252     }
253 }
254
255 static void
256 bin_init (bin_t *bin,
257           int    score)
258 {
259     bin->score = score;
260
261     bin->items_size = 0;
262     bin->num_items = 0;
263     bin->items = NULL;
264 }
265
266 static void
267 bin_fini (bin_t *bin)
268 {
269     free (bin->items);
270 }
271
272 static void
273 bin_grow (bin_t *bin)
274 {
275     if (bin->items_size)
276         bin->items_size *= 2;
277     else
278         bin->items_size = 1;
279
280     bin->items = xrealloc (bin->items,
281                            bin->items_size * sizeof (item_t*));
282 }
283
284 static void
285 bin_add_item (bin_t     *bin,
286               item_t    *item)
287 {
288     assert (item->score == bin->score);
289
290     if (bin->num_items == bin->items_size)
291         bin_grow (bin);
292
293     bin->items[bin->num_items++] = item;
294 }
295
296 static void
297 bin_remove_item (bin_t  *bin,
298                  int     item_index)
299 {
300     /* Replace the current item with the last item, (no need to shift
301      * any more than that since we don't care about the order of the
302      * items within a bin). */
303     bin->num_items--;
304     if (bin->num_items)
305         bin->items[item_index] = bin->items[bin->num_items];
306 }
307
308 /* Find the index for an item within a bin.
309  *
310  * XXX: This is currently a linear search, so is a potential
311  * performance problem.
312  */
313 static int
314 bin_item_index (bin_t   *bin,
315                 item_t  *item)
316 {
317     int i;
318
319     for (i = 0; i < bin->num_items; i++)
320         if (bin->items[i] == item)
321             return i;
322
323     assert (0);
324 }
325
326 typedef int (item_match_predicate_t) (void *closure, item_t *item);
327
328 /* Return the number of items in the bin from the given category (or
329  * from all categories if category == NULL) */
330 static int
331 bin_num_items_matching (bin_t                   *bin,
332                         item_match_predicate_t  *predicate,
333                         void                    *closure)
334 {
335     int i, num_items = 0;
336
337     if (predicate == NULL)
338         return bin->num_items;
339
340     for (i = 0; i < bin->num_items; i++)
341         if ((predicate) (closure, bin->items[i]))
342             num_items++;
343
344     return num_items;
345 }
346
347 static void
348 mnemon_init (mnemon_t *mnemon)
349 {
350     char *home;
351
352     home = getenv ("HOME");
353     if (home == NULL)
354         home = "";
355
356     xasprintf (&mnemon->dir_name, "%s/.mnemon", getenv ("HOME"));
357
358     mnemon->categories_size = 0;
359     mnemon->num_categories = 0;
360     mnemon->categories = NULL;
361
362     mnemon->bins_size = 0;
363     mnemon->num_bins = 0;
364     mnemon->bins = NULL;
365
366     mnemon->to_introduce = 10;
367     mnemon->to_master = 0;
368     mnemon->unlearned = 0;
369     mnemon->mastered = -1;
370 }
371
372 static void
373 mnemon_fini (mnemon_t *mnemon)
374 {
375     int i;
376
377     for (i = 0; i < mnemon->num_bins; i++)
378         bin_fini (&mnemon->bins[i]);
379     free (mnemon->bins);
380
381     for (i = 0; i < mnemon->num_categories; i++)
382         category_fini (&mnemon->categories[i]);
383     free (mnemon->categories);
384
385     free (mnemon->dir_name);
386 }
387
388 static void
389 mnemon_categories_grow (mnemon_t *mnemon)
390 {
391     if (mnemon->categories_size)
392         mnemon->categories_size *= 2;
393     else
394         mnemon->categories_size = 1;
395
396     mnemon->categories = xrealloc (mnemon->categories,
397                                    mnemon->categories_size * sizeof (category_t));
398 }
399
400 /* Get a category by name if it exists */
401 static category_t *
402 mnemon_get_category_if_exists (mnemon_t     *mnemon,
403                                const char   *name)
404 {
405     int i;
406
407     for (i = 0; i < mnemon->num_categories; i++)
408         if (strcmp (mnemon->categories[i].name, name) == 0)
409             return &mnemon->categories[i];
410
411     return NULL;
412 }
413
414 /* Get a category by name, creating new one if necessary. */
415 static category_t *
416 mnemon_get_category (mnemon_t   *mnemon,
417                      const char *name)
418 {
419     category_t *category;
420
421     category = mnemon_get_category_if_exists (mnemon, name);
422     if (category)
423         return category;
424
425     mnemon_categories_grow (mnemon);
426
427     category = &mnemon->categories[mnemon->num_categories++];
428
429     category_init (category, name);
430
431     return category;
432 }
433
434 static void
435 mnemon_bins_grow (mnemon_t *mnemon)
436 {
437     if (mnemon->bins_size)
438         mnemon->bins_size *= 2;
439     else
440         mnemon->bins_size = 1;
441
442     mnemon->bins = xrealloc (mnemon->bins,
443                              mnemon->bins_size * sizeof (bin_t));
444 }
445
446 static bin_t *
447 mnemon_get_bin (mnemon_t        *mnemon,
448                 int              score)
449 {
450     int i;
451     bin_t *bin;
452
453     for (i = 0; i < mnemon->num_bins; i++)
454         if (mnemon->bins[i].score == score)
455             return &mnemon->bins[i];
456         else if (mnemon->bins[i].score > score)
457             break;
458
459     if (mnemon->num_bins == mnemon->bins_size)
460         mnemon_bins_grow (mnemon);
461
462     bin = &mnemon->bins[i];
463
464     /* Make room to insert new bin at its sorted location. */
465     if (i < mnemon->num_bins)
466         memmove (bin + 1, bin, (mnemon->num_bins - i) * sizeof (bin_t));
467     mnemon->num_bins++;
468
469     bin_init (bin, score);
470
471     return bin;
472 }
473
474 static void
475 mnemon_remove_bin (mnemon_t     *mnemon,
476                    bin_t        *bin)
477 {
478     int i = bin - mnemon->bins;
479
480     bin_fini (bin);
481
482     memmove (bin, bin + 1, (mnemon->num_bins - i) * sizeof (bin_t));
483     mnemon->num_bins--;
484 }
485
486 static void
487 chomp (char *s)
488 {
489     int len = strlen (s);
490     if (len == 0)
491         return;
492     if (s[len - 1] == '\n')
493         s[len - 1] = '\0';
494 }
495
496 static char *
497 trim_space (char *string)
498 {
499     char *s;
500
501     s = string;
502     while (*s && isspace (*s))
503         s++;
504
505     string = s;
506
507     s = string + strlen (string) - 1;
508     while (s > string && isspace (*s)) {
509         *s = '\0';
510         s--;
511     }
512
513     return string;
514 }
515
516 static void
517 mnemon_load_category (mnemon_t          *mnemon,
518                       const char        *name)
519 {
520     FILE *file;
521     char *line = NULL, *end;
522     size_t line_size = 0;
523     ssize_t bytes_read;
524     int line_count = 0;
525     char *path;
526     category_t *category;
527     int i;
528
529     path = xmalloc (strlen (mnemon->dir_name) + 1 + strlen (name) + 1);
530     sprintf (path, "%s/%s", mnemon->dir_name, name);
531
532     file = fopen (path, "r");
533     if (file == NULL) {
534         fprintf (stderr, "Error: Failed to open %s: %s\n",
535                  path, strerror (errno));
536         exit (1);
537     }
538
539     category = mnemon_get_category (mnemon, name);
540
541 #define READ_LINE do {                                  \
542     bytes_read = getline (&line, &line_size, file);     \
543     if (bytes_read == -1)                               \
544         goto END_OF_FILE;                               \
545     line_count++;                                       \
546     chomp (line);                                       \
547 } while (0)
548
549     /* Parse options */
550     while (1) {
551         char *name, *equal, *value;
552
553         /* Ignore blank lines */
554         READ_LINE;
555         if (*line == '\0')
556             continue;
557
558         /* An initial digit means we hit an item. Trigger the
559          * spaghetti machine. */
560         if (*line >= '0' && *line <= '9')
561             goto PARSE_BIN;
562
563         equal = strchr (line, '=');
564         if (equal == NULL) {
565             fprintf (stderr, "Malformed option, (expected name=value): \"%s\" at %s:%d\n",
566                      line, path, line_count);
567             exit (1);
568         }
569
570         value = equal + 1;
571         name = line;
572         *equal = '\0';
573
574         name = trim_space (name);
575         value = trim_space (value);
576
577         if (strcmp (name, "order") == 0) {
578             if (strcmp (value, "sequential") == 0) {
579                 category->order = CATEGORY_ORDER_SEQUENTIAL;
580             } else if (strcmp (value, "random") == 0) {
581                 category->order = CATEGORY_ORDER_RANDOM;
582             } else {
583                 fprintf (stderr, "Unknown value for \"order\" option \"%s\" at %s:%d\n",
584                          value, path, line_count);
585                 exit (1);
586             }
587         } else {
588             fprintf (stderr, "Unknown option %s at %s:%d\n",
589                      name, path, line_count);
590             exit (1);
591         }
592     }
593
594     /* Parse items */
595     while (1) {
596         int score;
597         char *challenge, *response;
598
599         /* Ignore blank lines */
600         READ_LINE;
601         if (*line == '\0')
602             continue;
603
604         /* Read bin number */
605       PARSE_BIN:
606         score = strtol (line, &end, 10);
607         if (*end != '\0') {
608             fprintf (stderr, "Failed to parse bin number from \"%s\" at %s:%d\n",
609                      line, path, line_count);
610             exit (1);
611         }
612
613         /* Read challenge */
614         READ_LINE;
615         challenge = strdup (line);
616
617         /* Read response */
618         READ_LINE;
619         response = line;
620
621         category_add_item (category, score, challenge, response);
622
623         free (challenge);
624     }
625   END_OF_FILE:
626
627     free (line);
628     fclose (file);
629     free (path);
630
631     /* Resize category items to fit exactly. */
632     category->items_size = category->num_items;
633     category->items = xrealloc (category->items, category->items_size * sizeof (item_t));
634
635     /* Now that the category is completely loaded, with stable
636      * pointers to every item, we can add each item to its appropriate
637      * bin. */
638     for (i = 0; i < category->num_items; i++) {
639         item_t *item = &category->items[i];
640         bin_t *bin = mnemon_get_bin (mnemon, item->score);
641
642         bin_add_item (bin, item);
643     }
644 }
645
646 static void
647 mnemon_load (mnemon_t *mnemon)
648 {
649     DIR *dir;
650     struct dirent *dirent;
651
652     dir = opendir (mnemon->dir_name);
653     if (dir == NULL) {
654         fprintf (stderr, "Error: Failed to open directory %s: %s\n",
655                  mnemon->dir_name, strerror (errno));
656         exit (1);
657     }
658
659     while (1) {
660         dirent = readdir (dir);
661         if (dirent == NULL)
662             break;
663
664         if (dirent->d_type == DT_REG) {
665             /* Ignore files matching *~, (yes, this shouldn't be
666              * hard-coded in such an ad-hoc way, but there you go. */
667             if (dirent->d_name[strlen(dirent->d_name)-1] != '~')
668                 mnemon_load_category (mnemon, dirent->d_name);
669         }
670     }
671
672     closedir (dir);
673 }
674
675 static void
676 mnemon_save (mnemon_t *mnemon)
677 {
678     int i, err;
679     char *filename, *lock_filename;
680     FILE *file;
681     category_t *category;
682
683     for (i = 0; i < mnemon->num_categories; i++) {
684         category = &mnemon->categories[i];
685
686         xasprintf (&filename, "%s/%s",
687                    mnemon->dir_name, category->name);
688         xasprintf (&lock_filename, "%s/.#%s",
689                    mnemon->dir_name, category->name);
690
691         file = fopen (lock_filename, "w");
692         if (file == NULL) {
693             fprintf (stderr, "Error: Failed to open %s for writing: %s\n",
694                      lock_filename, strerror (errno));
695             continue;
696         }
697
698         category_print (category, file);
699
700         fclose (file);
701
702         err = rename (lock_filename, filename);
703         if (err < 0) {
704             fprintf (stderr, "Error: Failed to rename %s to %s: %s\n",
705                      lock_filename, filename, strerror (errno));
706             continue;
707         }
708
709         free (filename);
710         free (lock_filename);
711     }
712 }
713
714 /* Return a uniformly-distributed pseudo-random integer within the
715  * range:
716  *
717  *      0 <= result < num_values
718  */
719 static int
720 rand_within (int num_values)
721 {
722     return (int) (num_values * (rand() / (RAND_MAX + 1.0)));
723 }
724
725 /* Return an exponentially-distributed pseudo-random integer within
726  * the range:
727  *
728  *      0 <= result < num_values
729  *
730  * The distribution is such that each successively larger value will
731  * occur with a probability of half of the previous value.
732  */
733 static int
734 rand_within_exponential (int num_values)
735 {
736     static int r;
737     static uint32_t mask = 0;
738     int ones;
739     int bit;
740
741     /* Optimize the constant case. */
742     if (num_values == 1)
743         return 0;
744
745     ones = 0;
746
747     do {
748         if (mask == 0) {
749             r = rand ();
750             mask = 1 << 31;
751             while (mask > RAND_MAX)
752                 mask >>= 1;
753         }
754         bit = r & mask;
755         mask >>= 1;
756         if (bit) {
757             ones++;
758             if (ones == num_values)
759                 ones = 0;
760         }
761     } while (bit);
762
763     return ones;
764 }
765
766 /* Find the category to which an item belongs. */
767 static category_t *
768 mnemon_item_category (mnemon_t  *mnemon,
769                       item_t    *item)
770 {
771     category_t *category;
772     int i, item_index;
773
774     for (i = 0; i < mnemon->num_categories; i++) {
775         category = &mnemon->categories[i];
776         item_index = item - category->items;
777         if (item_index >= 0 && item_index < category->num_items)
778             return category;
779     }
780
781     assert (0);
782 }
783
784 typedef struct _item_in_category_closure
785 {
786     mnemon_t *mnemon;
787     category_t *category;
788 } item_in_category_closure_t;
789
790 static int
791 mnemon_item_in_category (void *closure, item_t *item)
792 {
793     item_in_category_closure_t *iicc = closure;
794     mnemon_t *mnemon = iicc->mnemon;
795     category_t *category = iicc->category;
796
797     return (mnemon_item_category (mnemon, item) == category);
798 }
799
800 static void
801 mnemon_select_item (mnemon_t     *mnemon,
802                     bin_t       **bin_ret,
803                     int          *item_index_ret)
804 {
805     int bin_index, item_index;
806     bin_t *bin;
807
808     bin_index = rand_within_exponential (mnemon->num_bins);
809
810     bin = &mnemon->bins[bin_index];
811
812     item_index = rand_within (bin->num_items);
813
814     if (bin->score == 0) {
815         category_t *category;
816         item_t *item;
817
818         item = bin->items[item_index];
819
820         category = mnemon_item_category (mnemon, item);
821
822         if (category->order == CATEGORY_ORDER_SEQUENTIAL) {
823             item = category_next_bin_zero_item (category);
824             if (item)
825                 item_index = bin_item_index (bin, item);
826         }
827     }
828
829     *bin_ret = bin;
830     *item_index_ret = item_index;
831 }
832
833
834 #define HISTOGRAM_ROW_FORMAT "%3d: %3d"
835 #define HISTOGRAM_BAR_WIDTH  63
836
837 static void
838 print_histogram_bar (double     size,
839                      double     max)
840 {
841     int units_per_cell = (int) ceil (max / HISTOGRAM_BAR_WIDTH);
842     static char const *boxes[8] = {
843         "█", "▉", "▊", "▋",
844         "▌", "▍", "▎", "▏"
845     };
846
847     while (size > units_per_cell) {
848         printf(boxes[0]);
849         size -= units_per_cell;
850     }
851
852     size /= units_per_cell;
853
854     if (size > 7.5/8.0)
855         printf(boxes[0]);
856     else if (size > 6.5/8.0)
857         printf(boxes[1]);
858     else if (size > 5.5/8.0)
859         printf(boxes[2]);
860     else if (size > 4.5/8.0)
861         printf(boxes[3]);
862     else if (size > 3.5/8.0)
863         printf(boxes[4]);
864     else if (size > 2.5/8.0)
865         printf(boxes[5]);
866     else if (size > 1.5/8.0)
867         printf(boxes[6]);
868     else if (size > 0.5/8.0)
869         printf(boxes[7]);
870
871     printf ("\n");
872 }
873
874 static void
875 mnemon_print_histogram (mnemon_t    *mnemon,
876                         const char  *category_name)
877 {
878     int i, last_score, max;
879     category_t *category = NULL;
880     bin_t *bin;
881     int num_items;
882     item_match_predicate_t *predicate = NULL;
883     void *closure = NULL;
884     item_in_category_closure_t item_in_category;
885
886     if (mnemon->num_bins == 0)
887         return;
888
889     if (category_name) {
890         category = mnemon_get_category_if_exists (mnemon, category_name);
891         if (category) {
892             predicate = mnemon_item_in_category;
893             item_in_category.mnemon = mnemon;
894             item_in_category.category = category;
895             closure = &item_in_category;
896         }
897     }
898
899     for (i = 0; i < mnemon->num_bins; i++) {
900         num_items = bin_num_items_matching (&mnemon->bins[i],
901                                             predicate, closure);
902         if (i == 0 || num_items > max)
903             max = num_items;
904     }
905
906     for (i = 0; i < mnemon->num_bins; i++) {
907         bin = &mnemon->bins[i];
908         if (i != 0)
909             while (bin->score - last_score > 1)
910                 printf (HISTOGRAM_ROW_FORMAT "\n", ++last_score, 0);
911         num_items = bin_num_items_matching (bin,
912                                             predicate, closure);
913         printf (HISTOGRAM_ROW_FORMAT " ", bin->score, num_items);
914         print_histogram_bar (num_items, max);
915         last_score = bin->score;
916     }
917 }
918
919 static void
920 mnemon_handle_command (mnemon_t         *mnemon,
921                        const char       *command)
922 {
923     const char *arg;
924     switch (command[0]) {
925         case 'h':
926             arg = command + 1;
927             while (*arg && isspace (*arg))
928                 arg++;
929             if (*arg == '\0')
930                 arg = NULL;
931             mnemon_print_histogram (mnemon, arg);
932             break;
933         default:
934             printf ("Unknown command: %s\n", command);
935             break;
936     }
937 }
938
939 static void
940 mnemon_handle_response (mnemon_t        *mnemon,
941                         bin_t           *bin,
942                         int              item_index,
943                         item_t          *item,
944                         const char      *response)
945 {
946     bool_t correct;
947
948     correct = (strcmp (response, item->response) == 0);
949
950     bin_remove_item (bin, item_index);
951
952     /* If the bin is now empty, we must remove it. Also if we just
953      * picked the last word we'll ever pick from the bin with
954      * score 0, then we can remove that as well. */
955     if (bin->num_items == 0 ||
956         (bin->score == 0 && mnemon->to_introduce == 0))
957     {
958         mnemon_remove_bin (mnemon, bin);
959     }
960
961     if (correct) {
962         item->score++;
963         /* We reserve an item score of 0 for an item that has
964          * never been asked. */
965         if (item->score == 0) {
966             item->score = 1;
967             mnemon->unlearned--;
968             printf ("You got it!");
969         } else if (item->score < 0) {
970             printf ("Yes---just give me %d more.",
971                     - item->score);
972         } else if (item->score == 1) {
973             printf ("On your first try, no less!");
974         } else {
975             printf ("Masterful (%dx).", item->score);
976             if (mnemon->to_master)
977                 mnemon->mastered++;
978         }
979     } else {
980         printf ("  %s is the correct answer.",
981                 item->response);
982         /* Penalize an incorrect response by forcing the score
983          * negative. */
984         if (item->score >= 0) {
985             if (item->score > 0)
986                 printf ( " Oops, you knew that, right?\n ");
987             mnemon->unlearned++;
988             /* We go to -2 to force a little extra reinforcement
989              * when re-learning an item, (otherwise, it will often
990              * get asked again immediately where it is easy to get
991              * a correct response without any learning). */
992             item->score = -2;
993         } else {
994             item->score--;
995         }
996     }
997
998     if (mnemon->to_introduce == 0 &&
999         mnemon->unlearned == 0 &&
1000         mnemon->to_master == 0)
1001     {
1002         mnemon->to_master = 10;
1003         mnemon->mastered = 0;
1004     }
1005
1006     printf (" ");
1007     if (mnemon->to_introduce)
1008         printf ("%d to come. ", mnemon->to_introduce);
1009     if (mnemon->unlearned)
1010         printf ("%d still unlearned. ", mnemon->unlearned);
1011     if (mnemon->to_master) {
1012         if (mnemon->mastered < mnemon->to_master)
1013             printf ("%d items to master",
1014                     mnemon->to_master - mnemon->mastered);
1015         else
1016             printf ("Great job!");
1017     }
1018     printf ("\n\n");
1019
1020     bin = mnemon_get_bin (mnemon, item->score);
1021
1022     bin_add_item (bin, item);
1023 }
1024
1025 static void
1026 mnemon_do_challenges (mnemon_t *mnemon)
1027 {
1028     bin_t *bin;
1029     int item_index;
1030     item_t *item;
1031     char *response;
1032     int i;
1033
1034     /* Count the number of items with negative scores. */
1035     mnemon->unlearned = 0;
1036     for (i = 0; i < mnemon->num_bins; i++) {
1037         bin = &mnemon->bins[i];
1038         if (bin->score >= 0)
1039             break;
1040         mnemon->unlearned += bin->num_items;
1041     }
1042
1043     mnemon->to_introduce -= mnemon->unlearned;
1044     if (mnemon->to_introduce < 0)
1045         mnemon->to_introduce = 0;
1046
1047     /* Get rid of bin with score of 0 if we aren't going to be
1048      * introducing anything from it. */
1049     if (mnemon->to_introduce == 0) {
1050         bin = mnemon_get_bin (mnemon, 0);
1051         mnemon_remove_bin (mnemon, bin);        
1052     }
1053
1054     if (mnemon->unlearned) {
1055         printf ("You've got %d items to learn already. ", mnemon->unlearned);
1056         if (mnemon->to_introduce)
1057             printf ("I'll introduce %d more as we go.", mnemon->to_introduce);
1058         printf ("\n");
1059     } else {
1060         printf ("Introducing %d new items.\n", mnemon->to_introduce);
1061     }
1062     printf ("\n");
1063
1064     do {
1065         mnemon_select_item (mnemon, &bin, &item_index);
1066         item = bin->items[item_index];
1067
1068         if (bin->score == 0)
1069             mnemon->to_introduce--;
1070
1071         while (1) {
1072             printf ("%s\n", item->challenge);
1073
1074             response = readline ("> ");
1075             /* Terminate on EOF */
1076             if (response == NULL) {
1077                 printf ("\n");
1078                 return;
1079             }
1080
1081             if (response[0] == '/')
1082                 mnemon_handle_command (mnemon, response + 1);
1083             else
1084                 break;
1085         }
1086
1087         mnemon_handle_response (mnemon, bin, item_index,
1088                                 item, response);
1089     } while (mnemon->mastered < mnemon->to_master);
1090 }
1091
1092 int
1093 main (int argc, char *argv[])
1094 {
1095     mnemon_t mnemon;
1096
1097     srand (time (NULL));
1098
1099     mnemon_init (&mnemon);
1100
1101     mnemon_load (&mnemon);
1102
1103     mnemon_do_challenges (&mnemon);
1104
1105     mnemon_save (&mnemon);
1106
1107     mnemon_fini (&mnemon);
1108
1109     return 0;
1110 }