X-Git-Url: https://git.cworth.org/git?p=mnemon;a=blobdiff_plain;f=main.c;h=819878919690335901fa2b9d58d9e02a74d55fe8;hp=643f49d77407b134d37e1983933b0c6157312436;hb=5ae11041e3b0f7c5a04795c71fe3794d663eb195;hpb=e213a74c688dc475578348ac5e0e438055d11311 diff --git a/main.c b/main.c index 643f49d..8198789 100644 --- a/main.c +++ b/main.c @@ -39,11 +39,541 @@ #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, + const char *challenge_type, + const char *challenge) +{ + const char *program; + char *command; + + if (strcmp (challenge_type, "text") == 0) { + 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. + */ + if (strcmp (challenge_type, "image") == 0) { + program = "xli -gamma 2.2"; + } else if (strcmp (challenge_type, "audio") == 0) { + program = "play"; + } else if (strcmp (challenge_type, "midi") == 0) { + program = "timidity -Os"; + } else if (strcmp (challenge_type, "text-to-speech") == 0) { + program = "mnemon-tts"; + } else { + fprintf (stderr, "Error: unsupported challenge type: %s\n", + challenge_type); + exit (1); + } + + 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), + const char *challenge_type) +{ + char * command; + + if (strcmp (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; + progress_t progress; void _load_categories() { @@ -62,7 +592,13 @@ main (int argc, char *argv[]) _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); @@ -73,7 +609,7 @@ main (int argc, char *argv[]) _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);