]> git.cworth.org Git - mnemon/blob - mnemon.c
Allow "/h <category>" to display histogram for a single category
[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 static void
327 mnemon_init (mnemon_t *mnemon)
328 {
329     char *home;
330
331     home = getenv ("HOME");
332     if (home == NULL)
333         home = "";
334
335     xasprintf (&mnemon->dir_name, "%s/.mnemon", getenv ("HOME"));
336
337     mnemon->categories_size = 0;
338     mnemon->num_categories = 0;
339     mnemon->categories = NULL;
340
341     mnemon->bins_size = 0;
342     mnemon->num_bins = 0;
343     mnemon->bins = NULL;
344
345     mnemon->to_introduce = 10;
346     mnemon->to_master = 0;
347     mnemon->unlearned = 0;
348     mnemon->mastered = -1;
349 }
350
351 static void
352 mnemon_fini (mnemon_t *mnemon)
353 {
354     int i;
355
356     for (i = 0; i < mnemon->num_bins; i++)
357         bin_fini (&mnemon->bins[i]);
358     free (mnemon->bins);
359
360     for (i = 0; i < mnemon->num_categories; i++)
361         category_fini (&mnemon->categories[i]);
362     free (mnemon->categories);
363
364     free (mnemon->dir_name);
365 }
366
367 static void
368 mnemon_categories_grow (mnemon_t *mnemon)
369 {
370     if (mnemon->categories_size)
371         mnemon->categories_size *= 2;
372     else
373         mnemon->categories_size = 1;
374
375     mnemon->categories = xrealloc (mnemon->categories,
376                                    mnemon->categories_size * sizeof (category_t));
377 }
378
379 /* Get a category by name if it exists */
380 static category_t *
381 mnemon_get_category_if_exists (mnemon_t     *mnemon,
382                                const char   *name)
383 {
384     int i;
385
386     for (i = 0; i < mnemon->num_categories; i++)
387         if (strcmp (mnemon->categories[i].name, name) == 0)
388             return &mnemon->categories[i];
389
390     return NULL;
391 }
392
393 /* Get a category by name, creating new one if necessary. */
394 static category_t *
395 mnemon_get_category (mnemon_t   *mnemon,
396                      const char *name)
397 {
398     category_t *category;
399
400     category = mnemon_get_category_if_exists (mnemon, name);
401     if (category)
402         return category;
403
404     mnemon_categories_grow (mnemon);
405
406     category = &mnemon->categories[mnemon->num_categories++];
407
408     category_init (category, name);
409
410     return category;
411 }
412
413 static void
414 mnemon_bins_grow (mnemon_t *mnemon)
415 {
416     if (mnemon->bins_size)
417         mnemon->bins_size *= 2;
418     else
419         mnemon->bins_size = 1;
420
421     mnemon->bins = xrealloc (mnemon->bins,
422                              mnemon->bins_size * sizeof (bin_t));
423 }
424
425 static bin_t *
426 mnemon_get_bin (mnemon_t        *mnemon,
427                 int              score)
428 {
429     int i;
430     bin_t *bin;
431
432     for (i = 0; i < mnemon->num_bins; i++)
433         if (mnemon->bins[i].score == score)
434             return &mnemon->bins[i];
435         else if (mnemon->bins[i].score > score)
436             break;
437
438     if (mnemon->num_bins == mnemon->bins_size)
439         mnemon_bins_grow (mnemon);
440
441     bin = &mnemon->bins[i];
442
443     /* Make room to insert new bin at its sorted location. */
444     if (i < mnemon->num_bins)
445         memmove (bin + 1, bin, (mnemon->num_bins - i) * sizeof (bin_t));
446     mnemon->num_bins++;
447
448     bin_init (bin, score);
449
450     return bin;
451 }
452
453 static void
454 mnemon_remove_bin (mnemon_t     *mnemon,
455                    bin_t        *bin)
456 {
457     int i = bin - mnemon->bins;
458
459     bin_fini (bin);
460
461     memmove (bin, bin + 1, (mnemon->num_bins - i) * sizeof (bin_t));
462     mnemon->num_bins--;
463 }
464
465 static void
466 chomp (char *s)
467 {
468     int len = strlen (s);
469     if (len == 0)
470         return;
471     if (s[len - 1] == '\n')
472         s[len - 1] = '\0';
473 }
474
475 static char *
476 trim_space (char *string)
477 {
478     char *s;
479
480     s = string;
481     while (*s && isspace (*s))
482         s++;
483
484     string = s;
485
486     s = string + strlen (string) - 1;
487     while (s > string && isspace (*s)) {
488         *s = '\0';
489         s--;
490     }
491
492     return string;
493 }
494
495 static void
496 mnemon_load_category (mnemon_t          *mnemon,
497                       const char        *name)
498 {
499     FILE *file;
500     char *line = NULL, *end;
501     size_t line_size = 0;
502     ssize_t bytes_read;
503     int line_count = 0;
504     char *path;
505     category_t *category;
506     int i;
507
508     path = xmalloc (strlen (mnemon->dir_name) + 1 + strlen (name) + 1);
509     sprintf (path, "%s/%s", mnemon->dir_name, name);
510
511     file = fopen (path, "r");
512     if (file == NULL) {
513         fprintf (stderr, "Error: Failed to open %s: %s\n",
514                  path, strerror (errno));
515         exit (1);
516     }
517
518     category = mnemon_get_category (mnemon, name);
519
520 #define READ_LINE do {                                  \
521     bytes_read = getline (&line, &line_size, file);     \
522     if (bytes_read == -1)                               \
523         goto END_OF_FILE;                               \
524     line_count++;                                       \
525     chomp (line);                                       \
526 } while (0)
527
528     /* Parse options */
529     while (1) {
530         char *name, *equal, *value;
531
532         /* Ignore blank lines */
533         READ_LINE;
534         if (*line == '\0')
535             continue;
536
537         /* An initial digit means we hit an item. Trigger the
538          * spaghetti machine. */
539         if (*line >= '0' && *line <= '9')
540             goto PARSE_BIN;
541
542         equal = strchr (line, '=');
543         if (equal == NULL) {
544             fprintf (stderr, "Malformed option, (expected name=value): \"%s\" at %s:%d\n",
545                      line, path, line_count);
546             exit (1);
547         }
548
549         value = equal + 1;
550         name = line;
551         *equal = '\0';
552
553         name = trim_space (name);
554         value = trim_space (value);
555
556         if (strcmp (name, "order") == 0) {
557             if (strcmp (value, "sequential") == 0) {
558                 category->order = CATEGORY_ORDER_SEQUENTIAL;
559             } else if (strcmp (value, "random") == 0) {
560                 category->order = CATEGORY_ORDER_RANDOM;
561             } else {
562                 fprintf (stderr, "Unknown value for \"order\" option \"%s\" at %s:%d\n",
563                          value, path, line_count);
564                 exit (1);
565             }
566         } else {
567             fprintf (stderr, "Unknown option %s at %s:%d\n",
568                      name, path, line_count);
569             exit (1);
570         }
571     }
572
573     /* Parse items */
574     while (1) {
575         int score;
576         char *challenge, *response;
577
578         /* Ignore blank lines */
579         READ_LINE;
580         if (*line == '\0')
581             continue;
582
583         /* Read bin number */
584       PARSE_BIN:
585         score = strtol (line, &end, 10);
586         if (*end != '\0') {
587             fprintf (stderr, "Failed to parse bin number from \"%s\" at %s:%d\n",
588                      line, path, line_count);
589             exit (1);
590         }
591
592         /* Read challenge */
593         READ_LINE;
594         challenge = strdup (line);
595
596         /* Read response */
597         READ_LINE;
598         response = line;
599
600         category_add_item (category, score, challenge, response);
601
602         free (challenge);
603     }
604   END_OF_FILE:
605
606     free (line);
607     fclose (file);
608     free (path);
609
610     /* Resize category items to fit exactly. */
611     category->items_size = category->num_items;
612     category->items = xrealloc (category->items, category->items_size * sizeof (item_t));
613
614     /* Now that the category is completely loaded, with stable
615      * pointers to every item, we can add each item to its appropriate
616      * bin. */
617     for (i = 0; i < category->num_items; i++) {
618         item_t *item = &category->items[i];
619         bin_t *bin = mnemon_get_bin (mnemon, item->score);
620
621         bin_add_item (bin, item);
622     }
623 }
624
625 static void
626 mnemon_load (mnemon_t *mnemon)
627 {
628     DIR *dir;
629     struct dirent *dirent;
630
631     dir = opendir (mnemon->dir_name);
632     if (dir == NULL) {
633         fprintf (stderr, "Error: Failed to open directory %s: %s\n",
634                  mnemon->dir_name, strerror (errno));
635         exit (1);
636     }
637
638     while (1) {
639         dirent = readdir (dir);
640         if (dirent == NULL)
641             break;
642
643         if (dirent->d_type == DT_REG) {
644             /* Ignore files matching *~, (yes, this shouldn't be
645              * hard-coded in such an ad-hoc way, but there you go. */
646             if (dirent->d_name[strlen(dirent->d_name)-1] != '~')
647                 mnemon_load_category (mnemon, dirent->d_name);
648         }
649     }
650
651     closedir (dir);
652 }
653
654 static void
655 mnemon_save (mnemon_t *mnemon)
656 {
657     int i, err;
658     char *filename, *lock_filename;
659     FILE *file;
660     category_t *category;
661
662     for (i = 0; i < mnemon->num_categories; i++) {
663         category = &mnemon->categories[i];
664
665         xasprintf (&filename, "%s/%s",
666                    mnemon->dir_name, category->name);
667         xasprintf (&lock_filename, "%s/.#%s",
668                    mnemon->dir_name, category->name);
669
670         file = fopen (lock_filename, "w");
671         if (file == NULL) {
672             fprintf (stderr, "Error: Failed to open %s for writing: %s\n",
673                      lock_filename, strerror (errno));
674             continue;
675         }
676
677         category_print (category, file);
678
679         fclose (file);
680
681         err = rename (lock_filename, filename);
682         if (err < 0) {
683             fprintf (stderr, "Error: Failed to rename %s to %s: %s\n",
684                      lock_filename, filename, strerror (errno));
685             continue;
686         }
687
688         free (filename);
689         free (lock_filename);
690     }
691 }
692
693 /* Return a uniformly-distributed pseudo-random integer within the
694  * range:
695  *
696  *      0 <= result < num_values
697  */
698 static int
699 rand_within (int num_values)
700 {
701     return (int) (num_values * (rand() / (RAND_MAX + 1.0)));
702 }
703
704 /* Return an exponentially-distributed pseudo-random integer within
705  * the range:
706  *
707  *      0 <= result < num_values
708  *
709  * The distribution is such that each successively larger value will
710  * occur with a probability of half of the previous value.
711  */
712 static int
713 rand_within_exponential (int num_values)
714 {
715     static int r;
716     static uint32_t mask = 0;
717     int ones;
718     int bit;
719
720     /* Optimize the constant case. */
721     if (num_values == 1)
722         return 0;
723
724     ones = 0;
725
726     do {
727         if (mask == 0) {
728             r = rand ();
729             mask = 1 << 31;
730             while (mask > RAND_MAX)
731                 mask >>= 1;
732         }
733         bit = r & mask;
734         mask >>= 1;
735         if (bit) {
736             ones++;
737             if (ones == num_values)
738                 ones = 0;
739         }
740     } while (bit);
741
742     return ones;
743 }
744
745 /* Find the category to which an item belongs. */
746 static category_t *
747 mnemon_item_category (mnemon_t  *mnemon,
748                       item_t    *item)
749 {
750     category_t *category;
751     int i, item_index;
752
753     for (i = 0; i < mnemon->num_categories; i++) {
754         category = &mnemon->categories[i];
755         item_index = item - category->items;
756         if (item_index >= 0 && item_index < category->num_items)
757             return category;
758     }
759
760     assert (0);
761 }
762
763 /* Return the number of items in the bin from the given category (or
764  * from all categories if category == NULL) */
765 static int
766 mnemon_bin_num_items_in_category (mnemon_t      *mnemon,
767                                   bin_t         *bin,
768                                   category_t    *category)
769 {
770     int i, num_items = 0;
771
772     if (category == NULL)
773         return bin->num_items;
774
775     for (i = 0; i < bin->num_items; i++)
776         if (mnemon_item_category (mnemon, bin->items[i]) == category)
777             num_items++;
778
779     return num_items;
780 }
781
782 static void
783 mnemon_select_item (mnemon_t     *mnemon,
784                     bin_t       **bin_ret,
785                     int          *item_index_ret)
786 {
787     int bin_index, item_index;
788     bin_t *bin;
789
790     bin_index = rand_within_exponential (mnemon->num_bins);
791
792     bin = &mnemon->bins[bin_index];
793
794     item_index = rand_within (bin->num_items);
795
796     if (bin->score == 0) {
797         category_t *category;
798         item_t *item;
799
800         item = bin->items[item_index];
801
802         category = mnemon_item_category (mnemon, item);
803
804         if (category->order == CATEGORY_ORDER_SEQUENTIAL) {
805             item = category_next_bin_zero_item (category);
806             if (item)
807                 item_index = bin_item_index (bin, item);
808         }
809     }
810
811     *bin_ret = bin;
812     *item_index_ret = item_index;
813 }
814
815
816 #define HISTOGRAM_ROW_FORMAT "%3d: %3d"
817 #define HISTOGRAM_BAR_WIDTH  63
818
819 static void
820 print_histogram_bar (double     size,
821                      double     max)
822 {
823     int units_per_cell = (int) ceil (max / HISTOGRAM_BAR_WIDTH);
824     static char const *boxes[8] = {
825         "█", "▉", "▊", "▋",
826         "▌", "▍", "▎", "▏"
827     };
828
829     while (size > units_per_cell) {
830         printf(boxes[0]);
831         size -= units_per_cell;
832     }
833
834     size /= units_per_cell;
835
836     if (size > 7.5/8.0)
837         printf(boxes[0]);
838     else if (size > 6.5/8.0)
839         printf(boxes[1]);
840     else if (size > 5.5/8.0)
841         printf(boxes[2]);
842     else if (size > 4.5/8.0)
843         printf(boxes[3]);
844     else if (size > 3.5/8.0)
845         printf(boxes[4]);
846     else if (size > 2.5/8.0)
847         printf(boxes[5]);
848     else if (size > 1.5/8.0)
849         printf(boxes[6]);
850     else if (size > 0.5/8.0)
851         printf(boxes[7]);
852
853     printf ("\n");
854 }
855
856 static void
857 mnemon_print_histogram (mnemon_t    *mnemon,
858                         const char  *category_name)
859 {
860     int i, last_score, max;
861     category_t *category = NULL;
862     bin_t *bin;
863     int num_items;
864
865     if (mnemon->num_bins == 0)
866         return;
867
868     if (category_name)
869         category = mnemon_get_category_if_exists (mnemon, category_name);
870
871     for (i = 0; i < mnemon->num_bins; i++) {
872         num_items = mnemon_bin_num_items_in_category (mnemon,
873                                                       &mnemon->bins[i],
874                                                       category);
875         if (i == 0 || num_items > max)
876             max = num_items;
877     }
878
879     for (i = 0; i < mnemon->num_bins; i++) {
880         bin = &mnemon->bins[i];
881         if (i != 0)
882             while (bin->score - last_score > 1)
883                 printf (HISTOGRAM_ROW_FORMAT "\n", ++last_score, 0);
884         num_items = mnemon_bin_num_items_in_category (mnemon,
885                                                       bin, category);
886         printf (HISTOGRAM_ROW_FORMAT " ", bin->score, num_items);
887         print_histogram_bar (num_items, max);
888         last_score = bin->score;
889     }
890 }
891
892 static void
893 mnemon_handle_command (mnemon_t         *mnemon,
894                        const char       *command)
895 {
896     const char *arg;
897     switch (command[0]) {
898         case 'h':
899             arg = command + 1;
900             while (*arg && isspace (*arg))
901                 arg++;
902             if (*arg == '\0')
903                 arg = NULL;
904             mnemon_print_histogram (mnemon, arg);
905             break;
906         default:
907             printf ("Unknown command: %s\n", command);
908             break;
909     }
910 }
911
912 static void
913 mnemon_handle_response (mnemon_t        *mnemon,
914                         bin_t           *bin,
915                         int              item_index,
916                         item_t          *item,
917                         const char      *response)
918 {
919     bool_t correct;
920
921     correct = (strcmp (response, item->response) == 0);
922
923     bin_remove_item (bin, item_index);
924
925     /* If the bin is now empty, we must remove it. Also if we just
926      * picked the last word we'll ever pick from the bin with
927      * score 0, then we can remove that as well. */
928     if (bin->num_items == 0 ||
929         (bin->score == 0 && mnemon->to_introduce == 0))
930     {
931         mnemon_remove_bin (mnemon, bin);
932     }
933
934     if (correct) {
935         item->score++;
936         /* We reserve an item score of 0 for an item that has
937          * never been asked. */
938         if (item->score == 0) {
939             item->score = 1;
940             mnemon->unlearned--;
941             printf ("You got it!");
942         } else if (item->score < 0) {
943             printf ("Yes---just give me %d more.",
944                     - item->score);
945         } else if (item->score == 1) {
946             printf ("On your first try, no less!");
947         } else {
948             printf ("Masterful (%dx).", item->score);
949             if (mnemon->to_master)
950                 mnemon->mastered++;
951         }
952     } else {
953         printf ("  %s is the correct answer.",
954                 item->response);
955         /* Penalize an incorrect response by forcing the score
956          * negative. */
957         if (item->score >= 0) {
958             if (item->score > 0)
959                 printf ( " Oops, you knew that, right?\n ");
960             mnemon->unlearned++;
961             /* We go to -2 to force a little extra reinforcement
962              * when re-learning an item, (otherwise, it will often
963              * get asked again immediately where it is easy to get
964              * a correct response without any learning). */
965             item->score = -2;
966         } else {
967             item->score--;
968         }
969     }
970
971     if (mnemon->to_introduce == 0 &&
972         mnemon->unlearned == 0 &&
973         mnemon->to_master == 0)
974     {
975         mnemon->to_master = 10;
976         mnemon->mastered = 0;
977     }
978
979     printf (" ");
980     if (mnemon->to_introduce)
981         printf ("%d to come. ", mnemon->to_introduce);
982     if (mnemon->unlearned)
983         printf ("%d still unlearned. ", mnemon->unlearned);
984     if (mnemon->to_master) {
985         if (mnemon->mastered < mnemon->to_master)
986             printf ("%d items to master",
987                     mnemon->to_master - mnemon->mastered);
988         else
989             printf ("Great job!");
990     }
991     printf ("\n\n");
992
993     bin = mnemon_get_bin (mnemon, item->score);
994
995     bin_add_item (bin, item);
996 }
997
998 static void
999 mnemon_do_challenges (mnemon_t *mnemon)
1000 {
1001     bin_t *bin;
1002     int item_index;
1003     item_t *item;
1004     char *response;
1005     int i;
1006
1007     /* Count the number of items with negative scores. */
1008     mnemon->unlearned = 0;
1009     for (i = 0; i < mnemon->num_bins; i++) {
1010         bin = &mnemon->bins[i];
1011         if (bin->score >= 0)
1012             break;
1013         mnemon->unlearned += bin->num_items;
1014     }
1015
1016     mnemon->to_introduce -= mnemon->unlearned;
1017     if (mnemon->to_introduce < 0)
1018         mnemon->to_introduce = 0;
1019
1020     /* Get rid of bin with score of 0 if we aren't going to be
1021      * introducing anything from it. */
1022     if (mnemon->to_introduce == 0) {
1023         bin = mnemon_get_bin (mnemon, 0);
1024         mnemon_remove_bin (mnemon, bin);        
1025     }
1026
1027     if (mnemon->unlearned) {
1028         printf ("You've got %d items to learn already. ", mnemon->unlearned);
1029         if (mnemon->to_introduce)
1030             printf ("I'll introduce %d more as we go.", mnemon->to_introduce);
1031         printf ("\n");
1032     } else {
1033         printf ("Introducing %d new items.\n", mnemon->to_introduce);
1034     }
1035     printf ("\n");
1036
1037     do {
1038         mnemon_select_item (mnemon, &bin, &item_index);
1039         item = bin->items[item_index];
1040
1041         if (bin->score == 0)
1042             mnemon->to_introduce--;
1043
1044         while (1) {
1045             printf ("%s\n", item->challenge);
1046
1047             response = readline ("> ");
1048             /* Terminate on EOF */
1049             if (response == NULL) {
1050                 printf ("\n");
1051                 return;
1052             }
1053
1054             if (response[0] == '/')
1055                 mnemon_handle_command (mnemon, response + 1);
1056             else
1057                 break;
1058         }
1059
1060         mnemon_handle_response (mnemon, bin, item_index,
1061                                 item, response);
1062     } while (mnemon->mastered < mnemon->to_master);
1063 }
1064
1065 int
1066 main (int argc, char *argv[])
1067 {
1068     mnemon_t mnemon;
1069
1070     srand (time (NULL));
1071
1072     mnemon_init (&mnemon);
1073
1074     mnemon_load (&mnemon);
1075
1076     mnemon_do_challenges (&mnemon);
1077
1078     mnemon_save (&mnemon);
1079
1080     mnemon_fini (&mnemon);
1081
1082     return 0;
1083 }