-/*
- * Copyright © 2006 Carl Worth
+/* mnemon - A memory training library
+ *
+ * Copyright © 2006,2011 Carl Worth
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2, or (at your option)
+ * the Free Software Foundation; either version 3, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."
*/
+#include "mnemon.h"
+
/* for asprintf */
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
+#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
assert (NOT_REACHED); \
} while (0)
-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_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;
-} category_t;
-
-typedef struct _mnemon {
- char *dir_name;
-
- int categories_size;
- int num_categories;
- category_t *categories;
-
- int bins_size;
- int num_bins;
- bin_t *bins;
-
- int to_introduce;
- int to_master;
- int unlearned;
- int mastered;
-} mnemon_t;
-
static void *
xmalloc (size_t size)
{
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, ...)
{
category->time_limit = 0.0;
category->bin_zero_head = 0;
category->challenge_type = CHALLENGE_TYPE_TEXT;
+ category->repeat = 0;
}
static void
case CHALLENGE_TYPE_AUDIO:
fprintf (file, "audio");
break;
+ case CHALLENGE_TYPE_MIDI:
+ fprintf (file, "midi");
+ break;
+ case CHALLENGE_TYPE_TEXT_TO_SPEECH:
+ fprintf (file, "text-to-speech");
+ break;
}
fprintf (file, "\n\n");
+ fprintf (file, "repeat = %d\n\n", category->repeat);
+
for (i = 0; i < category->num_items; i++) {
item = &category->items[i];
if (i != 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;
-}
-
-static void
+void
mnemon_init (mnemon_t *mnemon)
{
char *home;
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;
}
-static void
+void
mnemon_fini (mnemon_t *mnemon)
{
int i;
}
/* Get a category by name if it exists */
-static category_t *
+category_t *
mnemon_get_category_if_exists (mnemon_t *mnemon,
const char *name)
{
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);
return string;
}
-static void
+void
mnemon_load_category (mnemon_t *mnemon,
const char *name)
{
char *path;
category_t *category;
int i;
+ struct stat st;
path = xmalloc (strlen (mnemon->dir_name) + 1 + strlen (name) + 1);
sprintf (path, "%s/%s", mnemon->dir_name, name);
exit (1);
}
+ fstat (fileno(file), &st);
+ if (! S_ISREG(st.st_mode)) {
+ fprintf (stderr, "Error: File %s is not a regular file.\n", path);
+ exit (1);
+ }
+
category = mnemon_get_category (mnemon, name);
#define READ_LINE do { \
category->challenge_type = CHALLENGE_TYPE_IMAGE;
} else if (strcmp (value, "audio") == 0) {
category->challenge_type = CHALLENGE_TYPE_AUDIO;
+ } else if (strcmp (value, "midi") == 0) {
+ category->challenge_type = CHALLENGE_TYPE_MIDI;
+ } else if (strcmp (value, "text-to-speech") == 0) {
+ category->challenge_type = CHALLENGE_TYPE_TEXT_TO_SPEECH;
} else {
fprintf (stderr, "Unknown value for \"challenge\" option \"%s\" at %s:%d\n",
value, path, line_count);
exit (1);
}
+ } else if (strcmp (name, "repeat") == 0) {
+ if (strcmp (value, "0") == 0)
+ category->repeat = 0;
+ else
+ category->repeat = 1;
} else {
fprintf (stderr, "Unknown option %s at %s:%d\n",
name, path, line_count);
}
}
-static void
+void
mnemon_load (mnemon_t *mnemon)
{
DIR *dir;
closedir (dir);
}
-static void
+void
mnemon_save (mnemon_t *mnemon)
{
int i, err;
return ones;
}
-/* Find the category to which an item belongs. */
-static category_t *
+category_t *
mnemon_item_category (mnemon_t *mnemon,
item_t *item)
{
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;
- 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,
- category_t **category_ret)
+ category_t **category_ret,
+ int *introduced_ret)
{
int bin_index, item_index;
bin_t *bin;
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))
{
- mnemon->to_introduce--;
+ *introduced_ret = 1;
+ }
+ else
+ {
+ *introduced_ret = 0;
}
item_index = rand_within (bin->num_items);
*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");
-}
-
-static void
-mnemon_print_histogram (mnemon_t *mnemon,
- const char *category_name,
- int length)
+void
+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;
- 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);
- /* 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++;
- mnemon->to_master--;
/* 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;
- mnemon->unlearned--;
- mnemon->to_master--;
- 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);
- }
- } 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) {
- if (item->score > 0)
- printf (" Oops, you knew that, right? (%dx)\n ",
- item->score);
- mnemon->unlearned++;
- /* We add three here, (rather than just 2 to track the
- * change in the item's score below), as an extra
- * penalty. If the user is forgetting stuff learned
- * previously, then more time should be spent on mastering
- * than learning new items. */
- mnemon->to_master += item->score + 3;
/* 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
item->score = -2;
} else {
item->score--;
- mnemon->to_master++;
}
}
- 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);
}
-
-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;
- }
-
- xasprintf (&command, "%s %s/%s >/dev/null 2>&1 &",
- program,
- mnemon->dir_name,
- challenge);
- system (command);
- free (command);
-}
-
-static void
-mnemon_hide_challenge (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);
-}
-
-static 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->challenge_type == CHALLENGE_TYPE_AUDIO) {
- mnemon_show_challenge (mnemon, category->challenge_type,
- item->challenge);
- sleep (1);
- }
- } while (mnemon->to_introduce ||
- mnemon->unlearned ||
- mnemon->to_master > 0);
-}
-
-int
-main (int argc, char *argv[])
-{
- mnemon_t mnemon;
- char *response;
-
- srand (time (NULL));
-
- mnemon_init (&mnemon);
-
- mnemon_load (&mnemon);
-
- mnemon_do_challenges (&mnemon);
-
- mnemon_save (&mnemon);
-
- mnemon_fini (&mnemon);
-
- mnemon_init (&mnemon);
- mnemon_load (&mnemon);
-
- printf ("Great job.\nHere are your current results:\n");
- mnemon_print_histogram (&mnemon, NULL, 0);
- response = readline ("Press enter to quit.\n");
- free (response);
-
- mnemon_fini (&mnemon);
-
- return 0;
-}