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