]> git.cworth.org Git - mnemon/commitdiff
Further separation of mnemon main program from mnemon library.
authorCarl Worth <cworth@cworth.org>
Mon, 26 Sep 2011 04:56:38 +0000 (21:56 -0700)
committerCarl Worth <cworth@cworth.org>
Mon, 26 Sep 2011 05:10:57 +0000 (22:10 -0700)
As I'm starting to generate a real interface here, I continue to find
functionality that would violate that interface. Fix this by moving things
up into the main program as necessary to keep the interface fairly clean.

For example, all session-based notions, (such as to_introduce,
to_master, etc.), are now part of a new progress_t within main.c
instead of being part of mnemon_t in the library, (since one program
might have a vey different notion of what a session is than another).

main.c
mnemon.c
mnemon.h

diff --git a/main.c b/main.c
index 643f49d77407b134d37e1983933b0c6157312436..b583d394b7010724862b8f33a803b90b29652268 100644 (file)
--- a/main.c
+++ b/main.c
 
 #include "mnemon.h"
 
 
 #include "mnemon.h"
 
+#define ASSERT_NOT_REACHED             \
+do {                                   \
+    static const int NOT_REACHED = 0;  \
+    assert (NOT_REACHED);              \
+} while (0)
+
+typedef struct progress {
+    int to_introduce;
+    int to_master;
+    int unlearned;
+    int mastered;
+} progress_t;
+
+static char *
+xstrndup (const char *s, size_t n)
+{
+    char *ret;
+
+    ret = strndup (s, n);
+    if (ret == NULL) {
+       fprintf (stderr, "Error: out of memory\n");
+       exit (1);
+    }
+
+    return ret;
+}
+
+static void
+xasprintf (char **strp, const char *fmt, ...)
+{
+    va_list ap;
+    int ret;
+
+    va_start (ap, fmt);
+    ret = vasprintf (strp, fmt, ap);
+    va_end (ap);
+
+    if (ret < 0) {
+       fprintf (stderr, "Error: out of memory\n");
+       exit (1);
+    }
+}
+
+static void
+_show_challenge (mnemon_t *mnemon,
+                challenge_type_t challenge_type,
+                const char *challenge)
+{
+    const char *program;
+    char *command;
+
+    if (challenge_type == CHALLENGE_TYPE_TEXT) {
+       printf ("%s\n", challenge);
+       return;
+    }
+
+    /* XXX: Yes, shelling out to system is total cheese. The planned
+     * fix here is to bring graphical display in process, (or at least
+     * have a custom external program that accepts image filenames on
+     * stdin.
+     */
+    switch (challenge_type) {
+    case CHALLENGE_TYPE_TEXT:
+       ASSERT_NOT_REACHED;
+       break;
+    case CHALLENGE_TYPE_IMAGE:
+       program = "xli -gamma 2.2";
+       break;
+    case CHALLENGE_TYPE_AUDIO:
+       program = "play";
+       break;
+    case CHALLENGE_TYPE_MIDI:
+       program = "timidity -Os";
+       break;
+    case CHALLENGE_TYPE_TEXT_TO_SPEECH:
+       program = "mnemon-tts";
+       break;
+    }
+
+    xasprintf (&command, "%s %s/%s >/dev/null 2>&1 &",
+              program,
+              mnemon->dir_name,
+              challenge);
+    system (command);
+    free (command);
+}
+
+static void
+_hide_challenge (unused (mnemon_t *mnemon),
+                challenge_type_t challenge_type)
+{
+    char * command;
+
+    if (challenge_type != CHALLENGE_TYPE_IMAGE)
+       return;
+
+    /* XXX: And this is just embarrassing (obviously wrong in several
+     * ways). Hopefully I'll amend away any commit that includes this.
+     */
+    xasprintf (&command, "killall xli");
+    system (command);
+    free (command);
+}
+
+typedef int (item_match_predicate_t) (void *closure, item_t *item);
+
+/* Return the number of items in the bin from the given category (or
+ * from all categories if category == NULL) */
+static int
+bin_num_items_matching (bin_t                  *bin,
+                       item_match_predicate_t  *predicate,
+                       void                    *closure)
+{
+    int i, num_items = 0;
+
+    if (predicate == NULL)
+       return bin->num_items;
+
+    for (i = 0; i < bin->num_items; i++)
+       if ((predicate) (closure, bin->items[i]))
+           num_items++;
+
+    return num_items;
+}
+
+typedef struct _item_in_category_closure
+{
+    mnemon_t *mnemon;
+    category_t *category;
+} item_in_category_closure_t;
+
+static int
+mnemon_item_in_category (void *closure, item_t *item)
+{
+    item_in_category_closure_t *iicc = closure;
+    mnemon_t *mnemon = iicc->mnemon;
+    category_t *category = iicc->category;
+
+    return (mnemon_item_category (mnemon, item) == category);
+}
+
+typedef struct _item_in_category_of_length_closure
+{
+    mnemon_t *mnemon;
+    category_t *category;
+    int length;
+} item_in_category_of_length_closure_t;
+
+static int
+mnemon_item_in_category_of_length (void *closure, item_t *item)
+{
+    item_in_category_of_length_closure_t *iicolc = closure;
+    mnemon_t *mnemon = iicolc->mnemon;
+    category_t *category = iicolc->category;
+    unsigned int length = iicolc->length;
+
+    if (mnemon_item_category (mnemon, item) != category)
+       return 0;
+
+    return strlen (item->challenge) == length;
+}
+
+#define HISTOGRAM_ROW_FORMAT "%3d: %3d"
+#define HISTOGRAM_BAR_WIDTH  63
+
+static void
+print_histogram_bar (double    size,
+                    double     max)
+{
+    int units_per_cell = (int) ceil (max / HISTOGRAM_BAR_WIDTH);
+    static char const *boxes[8] = {
+       "█", "▉", "▊", "▋",
+       "▌", "▍", "▎", "▏"
+    };
+
+    while (size > units_per_cell) {
+       printf(boxes[0]);
+       size -= units_per_cell;
+    }
+
+    size /= units_per_cell;
+
+    if (size > 7.5/8.0)
+       printf(boxes[0]);
+    else if (size > 6.5/8.0)
+       printf(boxes[1]);
+    else if (size > 5.5/8.0)
+       printf(boxes[2]);
+    else if (size > 4.5/8.0)
+       printf(boxes[3]);
+    else if (size > 3.5/8.0)
+       printf(boxes[4]);
+    else if (size > 2.5/8.0)
+       printf(boxes[5]);
+    else if (size > 1.5/8.0)
+       printf(boxes[6]);
+    else if (size > 0.5/8.0)
+       printf(boxes[7]);
+
+    printf ("\n");
+}
+
+/* Print a histogram showing the number of items in each bin.
+ *
+ * If category_name is not NULL, then only the items from the given
+ * category (matching a particular filename within the user's .mnemon
+ * directory) will be shown.
+ *
+ * If length is non zero, then only items with a challenge string of
+ * 'length' characters will be shown. (This is only useful for
+ * particular types of challenges, such as for showing anagram
+ * challenges of a given length).
+ *
+ * To see a histogram of all currently-loaded items, pass NULL for
+ * category and 0 for length.
+ *
+ * Note: Some bins may be removed entirely by (a misfeature side
+ * effect of) the mnemon_do_challenges function, (such as bin 0 being
+ * removed after the introduction phase is complete). An accurate
+ * histogram can be guaranteed by calling menmon_print_histogram
+ * immediately after calling mnemon_load.
+ */
+static void
+print_histogram (mnemon_t    *mnemon,
+                const char  *category_name,
+                int         length)
+{
+    int i, last_score, max;
+    category_t *category = NULL;
+    bin_t *bin;
+    int num_items;
+    item_match_predicate_t *predicate = NULL;
+    void *closure = NULL;
+    item_in_category_closure_t item_in_category;
+    item_in_category_of_length_closure_t item_in_category_of_length;
+
+    if (mnemon->num_bins == 0)
+       return;
+
+    if (category_name) {
+       category = mnemon_get_category_if_exists (mnemon, category_name);
+       if (category) {
+           if (length) {
+               predicate = mnemon_item_in_category_of_length;
+               item_in_category_of_length.mnemon = mnemon;
+               item_in_category_of_length.category = category;
+               item_in_category_of_length.length = length;
+               closure = &item_in_category_of_length;
+           } else {
+               predicate = mnemon_item_in_category;
+               item_in_category.mnemon = mnemon;
+               item_in_category.category = category;
+               closure = &item_in_category;
+           }
+       }
+    }
+
+    for (i = 0; i < mnemon->num_bins; i++) {
+       num_items = bin_num_items_matching (&mnemon->bins[i],
+                                           predicate, closure);
+       if (i == 0 || num_items > max)
+           max = num_items;
+    }
+
+    for (i = 0; i < mnemon->num_bins; i++) {
+       bin = &mnemon->bins[i];
+       if (i != 0)
+           while (bin->score - last_score > 1)
+               printf (HISTOGRAM_ROW_FORMAT "\n", ++last_score, 0);
+       num_items = bin_num_items_matching (bin,
+                                           predicate, closure);
+       printf (HISTOGRAM_ROW_FORMAT " ", bin->score, num_items);
+       print_histogram_bar (num_items, max);
+       last_score = bin->score;
+    }
+}
+
+static void
+_handle_command (mnemon_t              *mnemon,
+                const char     *command)
+{
+    const char *arg;
+    int len;
+    switch (command[0]) {
+       /* 'h' for histogram */
+       case 'h':
+       {
+           char *category = NULL;
+           int length = 0;
+
+           arg = command + 1;
+           arg += strspn (arg, " \t");
+           len = strcspn (arg, " \t");
+           if (len) {
+               category = xstrndup (arg, len);
+               arg += len;
+               arg += strspn (arg, " \t");
+               if (*arg)
+                   length = atoi (arg);
+           }
+           print_histogram (mnemon, category, length);
+       }
+       break;
+       /* 'r' for repeat */
+        case 'r':
+       {
+           /* Nothing necessary for repeating. */
+       }
+       break;
+        default:
+           printf ("Unknown command: %s\n", command);
+           break;
+    }
+}
+
+static void
+_handle_response (mnemon_t     *mnemon,
+                 bin_t         *bin,
+                 int            item_index,
+                 item_t        *item,
+                 const char    *response,
+                 double         response_time,
+                 double         time_limit,
+                 progress_t    *progress)
+{
+    bool_t correct;
+    int old_score = item->score;
+
+    correct = (strcmp (response, item->response) == 0);
+
+    if (! correct)
+    {
+           printf ("  %s is the correct answer.",
+                   item->response);
+    }
+
+    if (correct &&
+       (time_limit != 0.0 && response_time > time_limit))
+    {
+           printf ("Correct, but not quite quick enough (%0.2f seconds---needed %0.2f seconds)\n",
+                   response_time, time_limit);
+           correct = 0;
+    }
+
+    mnemon_score_item (mnemon, bin, item_index, correct);
+
+
+    if (correct) {
+       if (item->score < 0) {
+           printf ("Yes---just give me %d more.",
+                   - item->score);
+       } else if (item->score == 1) {
+           if (old_score < 0) {
+               progress->unlearned--;
+               printf ("You got it!");
+           } else {
+               printf ("On your first try, no less!");
+           }
+       } else {
+           printf ("Masterful (%dx).", item->score);
+           if (progress->to_master)
+               progress->to_master--;
+       }
+    } else {
+       if (old_score > 0) {
+           printf (" Oops, you knew that, right? (%dx)\n ",
+                   old_score);
+           progress->unlearned++;
+           /* We increase to_master here as an extra penalty. If the
+            * user is forgetting stuff learned previously, then more
+            * time should be spent on mastering than learning new
+            * items. Note that we only do this during the initial
+            * phase while new items are still being introduced. */
+           if (progress->to_introduce)
+               progress->to_master++;
+       }
+    }
+
+    printf (" ");
+    if (progress->to_introduce)
+       printf ("%d to come. ", progress->to_introduce);
+    if (progress->unlearned)
+       printf ("%d still unlearned. ", progress->unlearned);
+    if (progress->to_introduce == 0 && progress->to_master > 0)
+       printf ("%d items to master", progress->to_master);
+    printf ("\n\n");
+}
+
+/* A session of challenges consists of three phases, some of which may
+ * be entirely empty, as follows:
+ *    
+ * 1. The introduction phase
+ *  
+ *     This phase is controlled by the to_introduce counter which is
+ *     by default set to 10. It is decremented every time an item is
+ *     introduced from the bin with score 0, or (if there is no bin
+ *     with score 0), every time an item is introduced from the bin
+ *     with the lowest non-negative score of any bin.
+ *
+ * 2. The mastering phase
+ *  
+ *     This phase is controlled by the to_master counter which is
+ *     initially set to 10. It begins at the beginning of the session
+ *     so can run concurrently with the introduction phase. The
+ *     to_master counter is decremented every time an item with a
+ *     positive (non-zero) score is answered correctly. It is also
+ *     incremented every time an item with a positive (non-zero) score
+ *     is answered incorrectly during the introduction phase. If
+ *     perfect mastery is demonstrated, the mastering phase is likely
+ *     to be complete simultaneous with the introduction stage. If the
+ *     user is really struggling with mastery, the mastering phase
+ *     will extend long after the introduction phase is over. But
+ *     since we never incremeent to_master after the introduction
+ *     phase is over, the user cannot build an infinite snowball of
+ *     to_master items and have to give up in despair.
+ *
+ * 3. The solidifying phase
+ *  
+ *     This final phase continues after the mastering phase for as
+ *     long as any items with a negative score remain. The idea here
+ *     is that we want to quickly give the reinforcement from a missed
+ *     item in the current session. Also, there's a bit of a challenge
+ *     to the user to demonstrate good mastery of any non-negative
+ *     items presented so that the phase actually terminates. It's
+ *     possible for this phase to extend for an arbitrary amount of
+ *     time, but not very likely, (since the negative items are chosen
+ *     preferentially and the user will continue to see the correct
+ *     answers to them over and over).
+ *
+ * This function returns after all three phases are complete.
+ *
+ * The user's progress (the movement of items to various new bins) is
+ * kept only in memory. In order to save this progress to disk, the
+ * caller must call mnemon_save.
+ */
+static void
+_do_challenges (mnemon_t *mnemon, progress_t *progress)
+{
+    bin_t *bin;
+    int item_index;
+    item_t *item;
+    category_t *category;
+    char *response;
+    int i;
+
+    /* Count the number of items with negative scores. */
+    progress->unlearned = 0;
+    for (i = 0; i < mnemon->num_bins; i++) {
+       bin = &mnemon->bins[i];
+       if (bin->score >= 0)
+           break;
+       progress->unlearned += bin->num_items;
+    }
+
+    progress->to_introduce -= progress->unlearned;
+    if (progress->to_introduce < 0)
+       progress->to_introduce = 0;
+
+    /* Get rid of bin with score of 0 if we aren't going to be
+     * introducing anything from it. */
+    if (progress->to_introduce == 0) {
+       mnemon_remove_bin (mnemon, 0);
+    }
+
+    if (progress->unlearned) {
+       printf ("You've got %d items to learn already. ", progress->unlearned);
+       if (progress->to_introduce)
+           printf ("I'll introduce %d more as we go.", progress->to_introduce);
+       printf ("\n");
+    } else {
+       printf ("Introducing %d new items.\n", progress->to_introduce);
+    }
+    printf ("\n");
+
+    do {
+       struct timeval start, end;
+       int introduced;
+       
+       mnemon_select_item (mnemon, &bin, &item_index, &category, &introduced);
+       item = bin->items[item_index];
+
+       if (progress->to_introduce > 0 && introduced)
+           progress->to_introduce--;
+
+       while (1) {
+           if (category->time_limit > 0.0) {
+               response = readline ("The next one is timed. Press enter when ready:");
+               free (response);
+           }
+
+           _show_challenge (mnemon, category->challenge_type,
+                            item->challenge);
+
+           gettimeofday (&start, NULL);
+           response = readline ("> ");
+           gettimeofday (&end, NULL);
+
+           _hide_challenge (mnemon, category->challenge_type);
+
+           /* Terminate on EOF */
+           if (response == NULL) {
+               printf ("\n");
+               return;
+           }
+
+           if (response[0] == '/') {
+               _handle_command (mnemon, response + 1);
+               free (response);
+           } else {
+               break;
+           }
+       }
+
+       _handle_response (mnemon, bin, item_index,
+                         item, response,
+                         (end.tv_sec + end.tv_usec / 1e6) -
+                         (start.tv_sec + start.tv_usec / 1e6),
+                         category->time_limit, progress);
+       free (response);
+
+       /* Replay audio challenges for reinforcement. */
+       if (category->repeat)
+       {
+           _show_challenge (mnemon, category->challenge_type,
+                            item->challenge);
+           printf ("%s\n", item->challenge);
+           sleep (2);
+       }
+    } while (progress->to_introduce ||
+            progress->unlearned ||
+            progress->to_master > 0);
+}
+
 int
 main (int argc, char *argv[])
 {
     mnemon_t mnemon;
     char *response;
 int
 main (int argc, char *argv[])
 {
     mnemon_t mnemon;
     char *response;
+    progress_t progress;
 
     void _load_categories()
     {
 
     void _load_categories()
     {
@@ -62,7 +596,13 @@ main (int argc, char *argv[])
 
     _load_categories ();
 
 
     _load_categories ();
 
-    mnemon_do_challenges (&mnemon);
+    /* Set some reasonable defaults for a session */
+    progress.to_introduce = 10;
+    progress.to_master = 10;
+    progress.unlearned = 0;
+    progress.mastered = -1;
+
+    _do_challenges (&mnemon, &progress);
 
     mnemon_save (&mnemon);
 
 
     mnemon_save (&mnemon);
 
@@ -73,7 +613,7 @@ main (int argc, char *argv[])
     _load_categories ();
 
     printf ("Great job.\nHere are your current results:\n");
     _load_categories ();
 
     printf ("Great job.\nHere are your current results:\n");
-    mnemon_print_histogram (&mnemon, NULL, 0);
+    print_histogram (&mnemon, NULL, 0);
     response = readline ("Press enter to quit.\n");
     free (response);
 
     response = readline ("Press enter to quit.\n");
     free (response);
 
index f869993061de67ad9332bbe3b77a8753ab882310..0dc45380760d3d3592a9c875a11b343f100d50eb 100644 (file)
--- a/mnemon.c
+++ b/mnemon.c
@@ -45,53 +45,6 @@ do {                                 \
     assert (NOT_REACHED);              \
 } while (0)
 
     assert (NOT_REACHED);              \
 } while (0)
 
-#define unused(foo) foo __attribute__((unused))
-
-typedef int bool_t;
-
-typedef struct _item {
-    int score;
-    char *challenge;
-    char *response;
-} item_t;
-
-struct _bin {
-    int score;
-    int items_size;
-    int num_items;
-    item_t **items;
-};
-
-typedef enum {
-    CATEGORY_ORDER_RANDOM,
-    CATEGORY_ORDER_SEQUENTIAL
-} category_order_t;
-
-typedef enum {
-    CHALLENGE_TYPE_TEXT,
-    CHALLENGE_TYPE_IMAGE,
-    CHALLENGE_TYPE_AUDIO,
-    CHALLENGE_TYPE_MIDI,
-    CHALLENGE_TYPE_TEXT_TO_SPEECH
-} challenge_type_t;
-
-struct _category {
-    char *name;
-    int items_size;
-    int num_items;
-    item_t *items;
-
-    /* Support sequential introduction of items from bin 0 */
-    category_order_t order;
-    /* Support categories where responses are timed (0.0 == disable). */
-    double time_limit;
-    int bin_zero_head;
-    /* Support challenges of non-text types (image, audio, etc.) */
-    challenge_type_t challenge_type;
-    /* Whether to repeat afterwards (for a little extra reinforcement) */
-    bool_t repeat;
-};
-
 static void *
 xmalloc (size_t size)
 {
 static void *
 xmalloc (size_t size)
 {
@@ -134,20 +87,6 @@ xstrdup (const char *s)
     return ret;
 }
 
     return ret;
 }
 
-static char *
-xstrndup (const char *s, size_t n)
-{
-    char *ret;
-
-    ret = strndup (s, n);
-    if (ret == NULL) {
-       fprintf (stderr, "Error: out of memory\n");
-       exit (1);
-    }
-
-    return ret;
-}
-
 static void
 xasprintf (char **strp, const char *fmt, ...)
 {
 static void
 xasprintf (char **strp, const char *fmt, ...)
 {
@@ -375,27 +314,6 @@ bin_item_index (bin_t      *bin,
     assert (0);
 }
 
     assert (0);
 }
 
-typedef int (item_match_predicate_t) (void *closure, item_t *item);
-
-/* Return the number of items in the bin from the given category (or
- * from all categories if category == NULL) */
-static int
-bin_num_items_matching (bin_t                  *bin,
-                       item_match_predicate_t  *predicate,
-                       void                    *closure)
-{
-    int i, num_items = 0;
-
-    if (predicate == NULL)
-       return bin->num_items;
-
-    for (i = 0; i < bin->num_items; i++)
-       if ((predicate) (closure, bin->items[i]))
-           num_items++;
-
-    return num_items;
-}
-
 void
 mnemon_init (mnemon_t *mnemon)
 {
 void
 mnemon_init (mnemon_t *mnemon)
 {
@@ -414,11 +332,6 @@ mnemon_init (mnemon_t *mnemon)
     mnemon->bins_size = 0;
     mnemon->num_bins = 0;
     mnemon->bins = NULL;
     mnemon->bins_size = 0;
     mnemon->num_bins = 0;
     mnemon->bins = NULL;
-
-    mnemon->to_introduce = 10;
-    mnemon->to_master = 10;
-    mnemon->unlearned = 0;
-    mnemon->mastered = -1;
 }
 
 void
 }
 
 void
@@ -450,7 +363,7 @@ mnemon_categories_grow (mnemon_t *mnemon)
 }
 
 /* Get a category by name if it exists */
 }
 
 /* Get a category by name if it exists */
-static category_t *
+category_t *
 mnemon_get_category_if_exists (mnemon_t            *mnemon,
                               const char   *name)
 {
 mnemon_get_category_if_exists (mnemon_t            *mnemon,
                               const char   *name)
 {
@@ -523,11 +436,16 @@ mnemon_get_bin (mnemon_t  *mnemon,
     return bin;
 }
 
     return bin;
 }
 
-static void
-mnemon_remove_bin (mnemon_t    *mnemon,
-                  bin_t        *bin)
+void
+mnemon_remove_bin (mnemon_t *mnemon, int bin_number)
 {
 {
-    int i = bin - mnemon->bins;
+    bin_t *bin = mnemon_get_bin (mnemon, bin_number);
+    int i;
+
+    if (bin == NULL)
+       return;
+
+    i = bin - mnemon->bins;
 
     bin_fini (bin);
 
 
     bin_fini (bin);
 
@@ -857,8 +775,7 @@ rand_within_exponential (int num_values)
     return ones;
 }
 
     return ones;
 }
 
-/* Find the category to which an item belongs. */
-static category_t *
+category_t *
 mnemon_item_category (mnemon_t *mnemon,
                      item_t    *item)
 {
 mnemon_item_category (mnemon_t *mnemon,
                      item_t    *item)
 {
@@ -875,48 +792,12 @@ mnemon_item_category (mnemon_t    *mnemon,
     assert (0);
 }
 
     assert (0);
 }
 
-typedef struct _item_in_category_closure
-{
-    mnemon_t *mnemon;
-    category_t *category;
-} item_in_category_closure_t;
-
-static int
-mnemon_item_in_category (void *closure, item_t *item)
-{
-    item_in_category_closure_t *iicc = closure;
-    mnemon_t *mnemon = iicc->mnemon;
-    category_t *category = iicc->category;
-
-    return (mnemon_item_category (mnemon, item) == category);
-}
-
-typedef struct _item_in_category_of_length_closure
-{
-    mnemon_t *mnemon;
-    category_t *category;
-    int length;
-} item_in_category_of_length_closure_t;
-
-static int
-mnemon_item_in_category_of_length (void *closure, item_t *item)
-{
-    item_in_category_of_length_closure_t *iicolc = closure;
-    mnemon_t *mnemon = iicolc->mnemon;
-    category_t *category = iicolc->category;
-    unsigned int length = iicolc->length;
-
-    if (mnemon_item_category (mnemon, item) != category)
-       return 0;
-
-    return strlen (item->challenge) == length;
-}
-
-static void
+void
 mnemon_select_item (mnemon_t    *mnemon,
                    bin_t       **bin_ret,
                    int          *item_index_ret,
 mnemon_select_item (mnemon_t    *mnemon,
                    bin_t       **bin_ret,
                    int          *item_index_ret,
-                   category_t  **category_ret)
+                   category_t  **category_ret,
+                   int          *introduced_ret)
 {
     int bin_index, item_index;
     bin_t *bin;
 {
     int bin_index, item_index;
     bin_t *bin;
@@ -926,16 +807,19 @@ mnemon_select_item (mnemon_t       *mnemon,
     bin_index = rand_within_exponential (mnemon->num_bins);
     bin = &mnemon->bins[bin_index];
 
     bin_index = rand_within_exponential (mnemon->num_bins);
     bin = &mnemon->bins[bin_index];
 
-    /* The most intuitive understanding of the to_introduce counter is
-     * that it's tracking never-before-learned items as they are
-     * pulled from the bin with score 0. But that bin can become
-     * empty. So the refined rule is that we decrement to_introduce
-     * whenever we pull from the lowest-indexed bin with a
-     * non-negative score. */
-    if (mnemon->to_introduce && bin->score >=0 &&
+    /* The most intuitive understanding of the introduced flag that
+     * it's tracking never-before-learned items as they are pulled
+     * from the bin with score 0. But that bin can become empty. So
+     * the refined rule is that we also set introduced whenever we
+     * pull from the lowest-indexed bin with a non-negative score. */
+    if (bin->score >=0 &&
        (bin_index == 0 || mnemon->bins[bin_index-1].score < 0))
     {
        (bin_index == 0 || mnemon->bins[bin_index-1].score < 0))
     {
-       mnemon->to_introduce--;
+       *introduced_ret = 1;
+    }
+    else
+    {
+       *introduced_ret = 0;
     }
 
     item_index = rand_within (bin->num_items);
     }
 
     item_index = rand_within (bin->num_items);
@@ -956,205 +840,39 @@ mnemon_select_item (mnemon_t      *mnemon,
     *category_ret = category;
 }
 
     *category_ret = category;
 }
 
-
-#define HISTOGRAM_ROW_FORMAT "%3d: %3d"
-#define HISTOGRAM_BAR_WIDTH  63
-
-static void
-print_histogram_bar (double    size,
-                    double     max)
-{
-    int units_per_cell = (int) ceil (max / HISTOGRAM_BAR_WIDTH);
-    static char const *boxes[8] = {
-       "█", "▉", "▊", "▋",
-       "▌", "▍", "▎", "▏"
-    };
-
-    while (size > units_per_cell) {
-       printf(boxes[0]);
-       size -= units_per_cell;
-    }
-
-    size /= units_per_cell;
-
-    if (size > 7.5/8.0)
-       printf(boxes[0]);
-    else if (size > 6.5/8.0)
-       printf(boxes[1]);
-    else if (size > 5.5/8.0)
-       printf(boxes[2]);
-    else if (size > 4.5/8.0)
-       printf(boxes[3]);
-    else if (size > 3.5/8.0)
-       printf(boxes[4]);
-    else if (size > 2.5/8.0)
-       printf(boxes[5]);
-    else if (size > 1.5/8.0)
-       printf(boxes[6]);
-    else if (size > 0.5/8.0)
-       printf(boxes[7]);
-
-    printf ("\n");
-}
-
 void
 void
-mnemon_print_histogram (mnemon_t    *mnemon,
-                       const char  *category_name,
-                       int          length)
+mnemon_score_item (mnemon_t *mnemon,
+                  bin_t *bin,
+                  unsigned int item_index,
+                  bool_t correct)
 {
 {
-    int i, last_score, max;
-    category_t *category = NULL;
-    bin_t *bin;
-    int num_items;
-    item_match_predicate_t *predicate = NULL;
-    void *closure = NULL;
-    item_in_category_closure_t item_in_category;
-    item_in_category_of_length_closure_t item_in_category_of_length;
+    item_t *item;
 
 
-    if (mnemon->num_bins == 0)
+    if (item_index >= bin->num_items)
        return;
 
        return;
 
-    if (category_name) {
-       category = mnemon_get_category_if_exists (mnemon, category_name);
-       if (category) {
-           if (length) {
-               predicate = mnemon_item_in_category_of_length;
-               item_in_category_of_length.mnemon = mnemon;
-               item_in_category_of_length.category = category;
-               item_in_category_of_length.length = length;
-               closure = &item_in_category_of_length;
-           } else {
-               predicate = mnemon_item_in_category;
-               item_in_category.mnemon = mnemon;
-               item_in_category.category = category;
-               closure = &item_in_category;
-           }
-       }
-    }
-
-    for (i = 0; i < mnemon->num_bins; i++) {
-       num_items = bin_num_items_matching (&mnemon->bins[i],
-                                           predicate, closure);
-       if (i == 0 || num_items > max)
-           max = num_items;
-    }
-
-    for (i = 0; i < mnemon->num_bins; i++) {
-       bin = &mnemon->bins[i];
-       if (i != 0)
-           while (bin->score - last_score > 1)
-               printf (HISTOGRAM_ROW_FORMAT "\n", ++last_score, 0);
-       num_items = bin_num_items_matching (bin,
-                                           predicate, closure);
-       printf (HISTOGRAM_ROW_FORMAT " ", bin->score, num_items);
-       print_histogram_bar (num_items, max);
-       last_score = bin->score;
-    }
-}
-
-static void
-mnemon_handle_command (mnemon_t                *mnemon,
-                      const char       *command)
-{
-    const char *arg;
-    int len;
-    switch (command[0]) {
-       /* 'h' for histogram */
-       case 'h':
-       {
-           char *category = NULL;
-           int length = 0;
-
-           arg = command + 1;
-           arg += strspn (arg, " \t");
-           len = strcspn (arg, " \t");
-           if (len) {
-               category = xstrndup (arg, len);
-               arg += len;
-               arg += strspn (arg, " \t");
-               if (*arg)
-                   length = atoi (arg);
-           }
-           mnemon_print_histogram (mnemon, category, length);
-       }
-       break;
-       /* 'r' for repeat */
-        case 'r':
-       {
-           /* Nothing necessary for repeating. */
-       }
-       break;
-        default:
-           printf ("Unknown command: %s\n", command);
-           break;
-    }
-}
-
-static void
-mnemon_handle_response (mnemon_t       *mnemon,
-                       bin_t           *bin,
-                       int              item_index,
-                       item_t          *item,
-                       const char      *response,
-                       double           response_time,
-                       double           time_limit)
-{
-    bool_t correct;
-
-    correct = (strcmp (response, item->response) == 0);
-
+    item = bin->items[item_index];
     bin_remove_item (bin, item_index);
 
     bin_remove_item (bin, item_index);
 
-    /* If the bin is now empty, we must remove it. Also if we just
-     * picked the last word we'll ever pick from the bin with
-     * score 0, then we can remove that as well. */
-    if (bin->num_items == 0 ||
-       (bin->score == 0 && mnemon->to_introduce == 0))
+    /* If the bin is now empty, we must remove it. */
+    if (bin->num_items == 0)
     {
     {
-       mnemon_remove_bin (mnemon, bin);
+       mnemon_remove_bin (mnemon, bin->score);
     }
 
     }
 
-    if (correct &&
-       (time_limit == 0.0 || response_time < time_limit))
+    if (correct)
     {
        item->score++;
        /* We reserve an item score of 0 for an item that has
         * never been asked. */
     {
        item->score++;
        /* We reserve an item score of 0 for an item that has
         * never been asked. */
-       if (item->score == 0) {
+       if (item->score == 0)
            item->score = 1;
            item->score = 1;
-           mnemon->unlearned--;
-           printf ("You got it!");
-       } else if (item->score < 0) {
-           printf ("Yes---just give me %d more.",
-                   - item->score);
-       } else if (item->score == 1) {
-           printf ("On your first try, no less!");
-       } else {
-           printf ("Masterful (%dx).", item->score);
-           if (mnemon->to_master)
-               mnemon->to_master--;
-       }
-    } else {
-       if (! correct)
-           printf ("  %s is the correct answer.",
-                   item->response);
-       else
-           printf ("Correct, but not quite quick enough (%0.2f seconds---needed %0.2f seconds)\n",
-                   response_time, time_limit);
+    }
+    else
+    {
        /* Penalize an incorrect response by forcing the score
         * negative. */
        if (item->score >= 0) {
        /* Penalize an incorrect response by forcing the score
         * negative. */
        if (item->score >= 0) {
-           if (item->score > 0)
-               printf (" Oops, you knew that, right? (%dx)\n ",
-                       item->score);
-           mnemon->unlearned++;
-           /* We increase to_master here as an extra penalty. If the
-            * user is forgetting stuff learned previously, then more
-            * time should be spent on mastering than learning new
-            * items. Note that we only do this during the initial
-            * phase while new items are still being introduced. */
-           if (mnemon->to_introduce)
-               mnemon->to_master++;
            /* We go to -2 to force a little extra reinforcement
             * when re-learning an item, (otherwise, it will often
             * get asked again immediately where it is easy to get
            /* We go to -2 to force a little extra reinforcement
             * when re-learning an item, (otherwise, it will often
             * get asked again immediately where it is easy to get
@@ -1165,172 +883,7 @@ mnemon_handle_response (mnemon_t *mnemon,
        }
     }
 
        }
     }
 
-    printf (" ");
-    if (mnemon->to_introduce)
-       printf ("%d to come. ", mnemon->to_introduce);
-    if (mnemon->unlearned)
-       printf ("%d still unlearned. ", mnemon->unlearned);
-    if (mnemon->to_introduce == 0 && mnemon->to_master > 0)
-       printf ("%d items to master", mnemon->to_master);
-    printf ("\n\n");
-
     bin = mnemon_get_bin (mnemon, item->score);
 
     bin_add_item (bin, item);
 }
     bin = mnemon_get_bin (mnemon, item->score);
 
     bin_add_item (bin, item);
 }
-
-static void
-mnemon_show_challenge (mnemon_t *mnemon,
-                      challenge_type_t challenge_type,
-                      const char *challenge)
-{
-    const char *program;
-    char *command;
-
-    if (challenge_type == CHALLENGE_TYPE_TEXT) {
-       printf ("%s\n", challenge);
-       return;
-    }
-
-    /* XXX: Yes, shelling out to system is total cheese. The planned
-     * fix here is to bring graphical display in process, (or at least
-     * have a custom external program that accepts image filenames on
-     * stdin.
-     */
-    switch (challenge_type) {
-    case CHALLENGE_TYPE_TEXT:
-       ASSERT_NOT_REACHED;
-       break;
-    case CHALLENGE_TYPE_IMAGE:
-       program = "xli -gamma 2.2";
-       break;
-    case CHALLENGE_TYPE_AUDIO:
-       program = "play";
-       break;
-    case CHALLENGE_TYPE_MIDI:
-       program = "timidity -Os";
-       break;
-    case CHALLENGE_TYPE_TEXT_TO_SPEECH:
-       program = "mnemon-tts";
-       break;
-    }
-
-    xasprintf (&command, "%s %s/%s >/dev/null 2>&1 &",
-              program,
-              mnemon->dir_name,
-              challenge);
-    system (command);
-    free (command);
-}
-
-static void
-mnemon_hide_challenge (unused (mnemon_t *mnemon),
-                      challenge_type_t challenge_type)
-{
-    char * command;
-
-    if (challenge_type != CHALLENGE_TYPE_IMAGE)
-       return;
-
-    /* XXX: And this is just embarrassing (obviously wrong in several
-     * ways). Hopefully I'll amend away any commit that includes this.
-     */
-    xasprintf (&command, "killall xli");
-    system (command);
-    free (command);
-}
-
-void
-mnemon_do_challenges (mnemon_t *mnemon)
-{
-    bin_t *bin;
-    int item_index;
-    item_t *item;
-    category_t *category;
-    char *response;
-    int i;
-
-    /* Count the number of items with negative scores. */
-    mnemon->unlearned = 0;
-    for (i = 0; i < mnemon->num_bins; i++) {
-       bin = &mnemon->bins[i];
-       if (bin->score >= 0)
-           break;
-       mnemon->unlearned += bin->num_items;
-    }
-
-    mnemon->to_introduce -= mnemon->unlearned;
-    if (mnemon->to_introduce < 0)
-       mnemon->to_introduce = 0;
-
-    /* Get rid of bin with score of 0 if we aren't going to be
-     * introducing anything from it. */
-    if (mnemon->to_introduce == 0) {
-       bin = mnemon_get_bin (mnemon, 0);
-       mnemon_remove_bin (mnemon, bin);        
-    }
-
-    if (mnemon->unlearned) {
-       printf ("You've got %d items to learn already. ", mnemon->unlearned);
-       if (mnemon->to_introduce)
-           printf ("I'll introduce %d more as we go.", mnemon->to_introduce);
-       printf ("\n");
-    } else {
-       printf ("Introducing %d new items.\n", mnemon->to_introduce);
-    }
-    printf ("\n");
-
-    do {
-       struct timeval start, end;
-       
-       mnemon_select_item (mnemon, &bin, &item_index, &category);
-       item = bin->items[item_index];
-
-       while (1) {
-           if (category->time_limit > 0.0) {
-               response = readline ("The next one is timed. Press enter when ready:");
-               free (response);
-           }
-
-           mnemon_show_challenge (mnemon, category->challenge_type,
-                                  item->challenge);
-
-           gettimeofday (&start, NULL);
-           response = readline ("> ");
-           gettimeofday (&end, NULL);
-
-           mnemon_hide_challenge (mnemon, category->challenge_type);
-
-           /* Terminate on EOF */
-           if (response == NULL) {
-               printf ("\n");
-               return;
-           }
-
-           if (response[0] == '/') {
-               mnemon_handle_command (mnemon, response + 1);
-               free (response);
-           } else {
-               break;
-           }
-       }
-
-       mnemon_handle_response (mnemon, bin, item_index,
-                               item, response,
-                               (end.tv_sec + end.tv_usec / 1e6) -
-                               (start.tv_sec + start.tv_usec / 1e6),
-                               category->time_limit);
-       free (response);
-
-       /* Replay audio challenges for reinforcement. */
-       if (category->repeat)
-       {
-           mnemon_show_challenge (mnemon, category->challenge_type,
-                                  item->challenge);
-           printf ("%s\n", item->challenge);
-           sleep (2);
-       }
-    } while (mnemon->to_introduce ||
-            mnemon->unlearned ||
-            mnemon->to_master > 0);
-}
index d33cb742ac000cb2b461c9ac99bbc83142208c61..396c9951c9682f0aa23b517142a4fdd1b8bb868f 100644 (file)
--- a/mnemon.h
+++ b/mnemon.h
 #ifndef MNEMON_H_INCLUDED
 #define MNEMON_H_INCLUDED
 
 #ifndef MNEMON_H_INCLUDED
 #define MNEMON_H_INCLUDED
 
-typedef struct _bin bin_t;
-typedef struct _category category_t;
+#define unused(foo) foo __attribute__((unused))
+
+typedef int bool_t;
+
+typedef struct _item {
+    int score;
+    char *challenge;
+    char *response;
+} item_t;
+
+typedef struct _bin {
+    int score;
+    int items_size;
+    int num_items;
+    item_t **items;
+} bin_t;
+
+typedef enum {
+    CATEGORY_ORDER_RANDOM,
+    CATEGORY_ORDER_SEQUENTIAL
+} category_order_t;
+
+typedef enum {
+    CHALLENGE_TYPE_TEXT,
+    CHALLENGE_TYPE_IMAGE,
+    CHALLENGE_TYPE_AUDIO,
+    CHALLENGE_TYPE_MIDI,
+    CHALLENGE_TYPE_TEXT_TO_SPEECH
+} challenge_type_t;
+
+typedef struct _category {
+    char *name;
+    int items_size;
+    int num_items;
+    item_t *items;
+
+    /* Support sequential introduction of items from bin 0 */
+    category_order_t order;
+    /* Support categories where responses are timed (0.0 == disable). */
+    double time_limit;
+    int bin_zero_head;
+    /* Support challenges of non-text types (image, audio, etc.) */
+    challenge_type_t challenge_type;
+    /* Whether to repeat afterwards (for a little extra reinforcement) */
+    bool_t repeat;
+} category_t;
 
 typedef struct _mnemon {
     char *dir_name;
 
 typedef struct _mnemon {
     char *dir_name;
@@ -33,11 +77,6 @@ typedef struct _mnemon {
     int bins_size;
     int num_bins;
     bin_t *bins;
     int bins_size;
     int num_bins;
     bin_t *bins;
-
-    int to_introduce;
-    int to_master;
-    int unlearned;
-    int mastered;
 } mnemon_t;
 
 /* Initialize a new mnemon object. This function must be called before
 } mnemon_t;
 
 /* Initialize a new mnemon object. This function must be called before
@@ -65,13 +104,28 @@ mnemon_load_category (mnemon_t             *mnemon,
 void
 mnemon_load (mnemon_t *mnemon);
 
 void
 mnemon_load (mnemon_t *mnemon);
 
-/* Run a series of memory challenges acoording to the to_introduce and
- * to_master counters as set on the given mnemon object.
+/* Select the next (weighted) random item to challenge the user.
+ *
+ * This function provides four return values (and yes, that's
+ * exceedingly awkward and a simpler interface should be designed to
+ * replace this):
+ *
+ *     bin:            The bin from which the item was selected
+ *
+ *     item_index:     The index within the bin of the slected item
+ *
+ *     category:       The name of the category for this item
+ *
+ *     introduced:     A flag indicating whether this is a newly
+ *                     introduced item. Items from bin 0 always count
+ *                     as newly introduced. If there is no bin 0,
+ *                     then items from the lowest non-negative bin
+ *                     will be flagged as introduced.
  *
  *
- * The challenge system is designed to rapidly reinforce items needing
+ * The selection system is designed to rapidly reinforce items needing
  * to be learned and provide exponentially less reinforcement for
  * items as mastery is displayed. This is achieved by storing the
  * to be learned and provide exponentially less reinforcement for
  * items as mastery is displayed. This is achieved by storing the
- * items in a series of numberred bins.
+ * items in a series of numbered bins.
  *
  * Items start in bin 0 indicating that they have never been presented
  * to a user. When an item is presented to the user and answered
  *
  * Items start in bin 0 indicating that they have never been presented
  * to a user. When an item is presented to the user and answered
@@ -87,85 +141,66 @@ mnemon_load (mnemon_t *mnemon);
  * number is the most likely to be chosen, while each succesively-
  * higher-numbered bin has a probability one-half of that of the
  * previous bin.
  * number is the most likely to be chosen, while each succesively-
  * higher-numbered bin has a probability one-half of that of the
  * previous bin.
+ */
+void
+mnemon_select_item (mnemon_t    *mnemon,
+                   bin_t       **bin_ret,
+                   int          *item_index_ret,
+                   category_t  **category_ret,
+                   int          *introduced_ret);
+
+/* Update an item based on a user's answer (correct or incorrect).
+ *
+ * The bin and item_index should be exactly as returned by
+ * mnemon_select_item.  The correct flag should indicate whether the
+ * user answered the challenge correctly or not, (and should only
+ * count as correct if the answer was within the time limit, if any).
+ *
+ * The item will be moved from its current bin to a new bin based on
+ * whether the challenge was answered correctly. The bin updates are
+ * as follows:
+ *
+ * If the answer was correct:
+ *     Increase the bin number by 1
+ *     If the new bin number is 0, set it to 1 (0 bin is for new items)
+ *
+ * If the answer was incorrect:
+ *     If the old bin was positive, move to bin -2 (for extra training)
+ *     Otherwise decrease the bin number by 1
  *
  *
- * A session of challenges consists of three phases, some of which may
- * be entirely empty, as follows:
- *    
- * 1. The introduction phase
- *  
- *     This phase is controlled by the to_introduce counter which is
- *     by default set to 10. It is decremented every time an item is
- *     introduced from the bin with score 0, or (if there is no bin
- *     with score 0), every time an item is introduced from the bin
- *     with the lowest non-negative score of any bin.
- *
- * 2. The mastering phase
- *  
- *     This phase is controlled by the to_master counter which is
- *     initially set to 10. It begins at the beginning of the session
- *     so can run concurrently with the introduction phase. The
- *     to_master counter is decremented every time an item with a
- *     positive (non-zero) score is answered correctly. It is also
- *     incremented every time an item with a positive (non-zero) score
- *     is answered incorrectly during the introduction phase. If
- *     perfect mastery is demonstrated, the mastering phase is likely
- *     to be complete simultaneous with the introduction stage. If the
- *     user is really struggling with mastery, the mastering phase
- *     will extend long after the introduction phase is over. But
- *     since we never incremeent to_master after the introduction
- *     phase is over, the user cannot build an infinite snowball of
- *     to_master items and have to give up in despair.
- *
- * 3. The solidifying phase
- *  
- *     This final phase continues after the mastering phase for as
- *     long as any items with a negative score remain. The idea here
- *     is that we want to quickly give the reinforcement from a missed
- *     item in the current session. Also, there's a bit of a challenge
- *     to the user to demonstrate good mastery of any non-negative
- *     items presented so that the phase actually terminates. It's
- *     possible for this phase to extend for an arbitrary amount of
- *     time, but not very likely, (since the negative items are chosen
- *     preferentially and the user will continue to see the correct
- *     answers to them over and over).
- *
- * This function returns after all three phases are complete.
- *
- * The user's progress (the movement of items to various new bins) is
- * kept only in memory. In order to save this progress to disk, the
- * caller must call mnemon_save.
+ * Note: All item and bin movement is kept only in memory. In order to
+ * save this progress to disk, the caller must call mnemon_save.
  */
 void
  */
 void
-mnemon_do_challenges (mnemon_t *mnemon);
+mnemon_score_item (mnemon_t *mnemon,
+                  bin_t *bin,
+                  unsigned int item_index,
+                  bool_t correct);
 
 /* Save the user's progress by updating the category files in the
  * users .mnemon directory. */
 void
 mnemon_save (mnemon_t *mnemon);
 
 
 /* Save the user's progress by updating the category files in the
  * users .mnemon directory. */
 void
 mnemon_save (mnemon_t *mnemon);
 
-/* Print a histogram showing the number of items in each bin.
+/* Remove a bin of a particular number.
  *
  *
- * If category_name is not NULL, then only the items from the given
- * category (matching a particular filename within the user's .mnemon
- * directory) will be shown.
- *
- * If length is non zero, then only items with a challenge string of
- * 'length' characters will be shown. (This is only useful for
- * particular types of challenges, such as for showing anagram
- * challenges of a given length).
- *
- * To see a histogram of all currently-loaded items, pass NULL for
- * category and 0 for length.
- *
- * Note: Some bins may be removed entirely by (a misfeature side
- * effect of) the mnemon_do_challenges function, (such as bin 0 being
- * removed after the introduction phase is complete). An accurate
- * histogram can be guaranteed by calling menmon_print_histogram
- * immediately after calling mnemon_load.
+ * This can be useful in situations such as wanting to practice
+ * mastery of learned items without mixing in new items that the user
+ * has never seen before. This could be achieved by removing bin 0,
+ * for example.
  */
 void
  */
 void
-mnemon_print_histogram (mnemon_t    *mnemon,
-                       const char  *category_name,
-                       int          length);
+mnemon_remove_bin (mnemon_t *mnemon, int bin_number);
+
+/* Find the category to which an item belongs. */
+category_t *
+mnemon_item_category (mnemon_t *mnemon,
+                     item_t    *item);
+
+/* Get a category by name if it exists */
+category_t *
+mnemon_get_category_if_exists (mnemon_t            *mnemon,
+                              const char   *name);
+
 
 #endif
 
 #endif