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