Index: apps/plugins/CATEGORIES =================================================================== --- apps/plugins/CATEGORIES (revision 17940) +++ apps/plugins/CATEGORIES (working copy) @@ -1,4 +1,5 @@ alpine_cdc,apps +dualnback,games autostart,apps battery_bench,apps blackjack,games Index: apps/plugins/SOURCES =================================================================== --- apps/plugins/SOURCES (revision 17940) +++ apps/plugins/SOURCES (working copy) @@ -1,4 +1,5 @@ /* plugins common to all models */ +dualnback.c battery_bench.c chessclock.c credits.c Index: apps/plugins/dualnback.c =================================================================== --- apps/plugins/dualnback.c (revision 0) +++ apps/plugins/dualnback.c (revision 0) @@ -0,0 +1,606 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id: dualnback.c 17847 2008-06-28 18:10:04Z bagder $ + * + * Copyright (C) 2008 Joseph Garvinh + * + * 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 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "plugin.h" + +PLUGIN_HEADER + +#include "nback_c.c" +#include "nback_h.c" +#include "nback_k.c" +#include "nback_l.c" +#include "nback_q.c" +#include "nback_r.c" +#include "nback_s.c" +#include "nback_t.c" + +#ifdef BUTTON_OFF +#define NBACK_QUIT BUTTON_OFF +#endif +#ifdef BUTTON_POWER +#define NBACK_QUIT BUTTON_POWER +#endif +#ifdef BUTTON_MENU +#define NBACK_QUIT BUTTON_MENU +#endif + +#define MAX_BLOCK_LENGTH 100 + +static const struct plugin_api* rb; +static int screen_width; +static int screen_height; + +static int n_level = 2; + +struct stimulus { + int box_x; + int box_y; + int sound; +}; + +struct sound { + signed short* raw_data; + size_t size; +}; + +static struct sound sounds[8] = { { nback_c, sizeof(nback_c) }, + { nback_h, sizeof(nback_h) }, + { nback_k, sizeof(nback_k) }, + { nback_l, sizeof(nback_l) }, + { nback_q, sizeof(nback_q) }, + { nback_r, sizeof(nback_r) }, + { nback_s, sizeof(nback_s) }, + { nback_t, sizeof(nback_t) } }; + +static bool usb_detected = false; + +/* Enough to let you get to level n=70, farther than most people will likely + ever get. Need 20+n trials for each test. */ +static struct stimulus block[MAX_BLOCK_LENGTH]; + +void get_random_box(int* x, int* y) +{ + *y = rb->rand() % 3; + + if(*y != 1) + *x = rb->rand() % 3; + else { + *x = rb->rand() % 2 == 0 ? 0 : 2; /* Don't use middle square */ + } +} + +int get_random_sound() +{ + return rb->rand() % (sizeof(sounds) / sizeof(struct sound)); +} + +int line_height() +{ + int width, height; + rb->font_getstringsize("|", &width, &height, FONT_UI); + return height; +} + +int get_top_text_area() +{ + return line_height(); +} + +int get_bottom_text_area() +{ + return line_height(); +} + +void clear_top_text_area() +{ + rb->lcd_set_foreground(LCD_RGBPACK(0,0,0)); + rb->lcd_fillrect(0, 0, screen_width, get_top_text_area()); +} + +void clear_bottom_text_area() +{ + rb->lcd_set_foreground(LCD_RGBPACK(0,0,0)); + rb->lcd_fillrect(0, screen_height - get_bottom_text_area(), screen_width, screen_height); +} + +void clearBoxArea() +{ + rb->lcd_set_foreground(LCD_RGBPACK(0,0,0)); + rb->lcd_fillrect(0, get_top_text_area(), screen_width, screen_height - get_bottom_text_area()); +} + +void draw_box(int x, int y) +{ + int drawing_area = screen_height - get_bottom_text_area() - get_top_text_area(); + int box_height = drawing_area / 3; + int box_width = screen_width / 3; + + rb->lcd_set_foreground(LCD_RGBPACK(0,0,255)); + rb->lcd_fillrect(x * box_width, get_top_text_area() + y * box_height, box_width, box_height); +} + +int block_length() +{ + return 20 + n_level; +} + +enum in_common { NONE, AUDIO, POSITION, BOTH }; +static enum in_common block_template[MAX_BLOCK_LENGTH]; + +bool in_array(int* array, int size, int to_find) +{ + int i; + + for(i = 0; i < size; ++i) + if(array[i] == to_find) + return true; + + return false; +} + +void generate_block() +{ + int i; + int try; + int chosen[10]; + int sound, box_x, box_y; + + for(i = 0; i < block_length(); ++i) { + block_template[i] = NONE; + block[i].sound = -1; + block[i].box_x = -1; + block[i].box_y = -1; + } + + for(i = 0; i < 4; ++i) { + do { + try = rb->rand() % (block_length() - n_level); + } while(in_array(chosen, i, try)); + + block_template[i] = AUDIO; + } + + for(i = 0; i < 4; ++i) { + do { + try = rb->rand() % (block_length() - n_level); + } while(in_array(chosen, i + 4, try)); + + block_template[i] = POSITION; + } + + for(i = 0; i < 2; ++i) { + do { + try = rb->rand() % (block_length() - n_level); + } while(in_array(chosen, i + 8, try)); + + block_template[i] = BOTH; + } + + /* Build block according to template */ + for(i = block_length() - n_level; i >= 0; --i) { + sound = get_random_sound(); + get_random_box(&box_x, &box_y); + + block[i].sound = sound; + block[i].box_x = box_x; + block[i].box_y = box_y; + + switch(block_template[i]) { + case AUDIO: + sound = get_random_sound(); + block[i].sound = sound; + block[i + n_level].sound = sound; + break; + case POSITION: + get_random_box(&box_x, &box_y); + block[i].box_x = box_x; + block[i].box_y = box_y; + block[i + n_level].box_x = box_x; + block[i + n_level].box_y = box_y; + break; + case BOTH: + sound = get_random_sound(); + get_random_box(&box_x, &box_y); + + block[i].sound = sound; + block[i + n_level].sound = sound; + + block[i].box_x = box_x; + block[i].box_y = box_y; + block[i + n_level].box_x = box_x; + block[i + n_level].box_y = box_y; + break; + default: break; + } + } + + /* Fill in unfilled entries */ + for(i = 0; i < block_length(); ++i) { + if(block[i].sound == -1) { + do { + sound = get_random_sound(); + } while((i + n_level < block_length() && block[i + n_level].sound == sound) || + (i - n_level >= 0 && block[i - n_level].sound == sound)); + + block[i].sound = sound; + } + + if(block[i].box_x == -1) { + do { + get_random_box(&box_x, &box_y); + } while((i + n_level < block_length() && + block[i + n_level].box_x == box_x && + block[i + n_level].box_y == box_y) || + (i - n_level >= 0 && + block[i - n_level].box_x == box_x && + block[i - n_level].box_y == box_y)); + + block[i].box_x = box_x; + block[i].box_y = box_y; + } + } +} + +enum feedback_status { WRONG, NEUTRAL, CORRECT }; + +unsigned int feedback_color(enum feedback_status feedback) +{ + switch(feedback) { + case WRONG: + return LCD_RGBPACK(255, 0, 0); + case NEUTRAL: + return LCD_RGBPACK(255, 255, 255); + case CORRECT: + return LCD_RGBPACK(0, 255, 0); + } + + /* Blue is an error! */ + return LCD_RGBPACK(0, 0, 255); +} + +enum plugin_status nback_game() +{ + bool quit = false; + int button = BUTTON_NONE; + char nlevel_str[6]; + int nlevel_str_width, nlevel_str_height; + long show_stimulus_timer = 0, + hide_stimulus_timer = 0; + bool showing_stimulus = false; + int trial_num = -1; + enum feedback_status pos_status = NEUTRAL, aud_status = NEUTRAL; + const char* pos_match = "<-Position Match"; + const char* aud_match = "Audio Match->"; + int pos_match_width, pos_match_height, aud_match_width, aud_match_height; + int visual_targets_hit = 0, audio_targets_hit = 0, visual_mistakes = 0, audio_mistakes = 0; + char report_buffer[100]; + + rb->font_getstringsize(pos_match, &pos_match_width, &pos_match_height, FONT_UI); + rb->font_getstringsize(aud_match, &aud_match_width, &aud_match_height, FONT_UI); + + generate_block(); + + while(!quit) { + if(TIME_AFTER(*rb->current_tick, show_stimulus_timer)) { + ++trial_num; + + show_stimulus_timer = *rb->current_tick + 3*HZ; + hide_stimulus_timer = *rb->current_tick + HZ / 2; + + clearBoxArea(); + draw_box(block[trial_num].box_x, block[trial_num].box_y); + + rb->pcm_play_data(NULL, + (char *)sounds[block[trial_num].sound].raw_data, + sounds[block[trial_num].sound].size); + + showing_stimulus = true; + + pos_status = NEUTRAL; + aud_status = NEUTRAL; + } + + if(showing_stimulus && TIME_AFTER(*rb->current_tick, hide_stimulus_timer)) { + clearBoxArea(); + showing_stimulus = false; + } + + button = rb->button_get(false); + switch(button) { + case NBACK_QUIT: + quit = true; + return PLUGIN_OK; + case BUTTON_LEFT: // Position match + if(trial_num - n_level < 0) + break; + + if(block[trial_num].box_x + == block[trial_num - n_level].box_x + && + block[trial_num].box_y + == block[trial_num - n_level].box_y + && + pos_status == NEUTRAL) { + pos_status = CORRECT; + ++visual_targets_hit; + } + else { + pos_status = WRONG; + } + + break; + case BUTTON_RIGHT: // Audio match + if(trial_num - n_level < 0) + break; + + if(block[trial_num].sound + == block[trial_num - n_level].sound + && aud_status == NEUTRAL) { + aud_status = CORRECT; + ++audio_targets_hit; + } + else { + aud_status = WRONG; + } + + default: + if (rb->default_event_handler(button) == SYS_USB_CONNECTED) { + return PLUGIN_USB_CONNECTED; + } + break; + } + + nlevel_str[0] = 'N'; nlevel_str[1] = '='; + rb->snprintf(&nlevel_str[2], 4, "%d", n_level); + + clear_top_text_area(); + rb->font_getstringsize(nlevel_str, &nlevel_str_width, &nlevel_str_height, FONT_UI); + rb->lcd_set_foreground(LCD_RGBPACK(255,255,255)); + rb->lcd_putsxy(0, 0, nlevel_str); + + clear_bottom_text_area(); + rb->lcd_set_foreground(feedback_color(pos_status)); + rb->lcd_putsxy(0, screen_height - pos_match_height, pos_match); + rb->lcd_set_foreground(feedback_color(aud_status)); + rb->lcd_putsxy(screen_width - aud_match_width, screen_height - aud_match_height, aud_match); + + rb->lcd_update(); + + if(trial_num > block_length()) { + visual_mistakes = 6 - visual_targets_hit; + audio_mistakes = 6 - audio_targets_hit; + if(visual_mistakes < 3 && audio_mistakes < 3) + ++n_level; + if(visual_mistakes + audio_mistakes > 5 && n_level > 2) + --n_level; + + rb->lcd_clear_display(); + rb->snprintf(report_buffer, 100, "Visual matches: %d", visual_targets_hit); + rb->lcd_putsxy(0, 0, report_buffer); + rb->snprintf(report_buffer, 100, "Audio matches: %d", audio_targets_hit); + rb->lcd_putsxy(0, line_height(), report_buffer); + rb->snprintf(report_buffer, 100, "New N Level: %d", n_level); + rb->lcd_putsxy(0, line_height() * 2, report_buffer); + rb->snprintf(report_buffer, 100, "Press Any Button To Continue"); + rb->lcd_putsxy(0, line_height() * 4, report_buffer); + rb->lcd_update(); + + rb->button_get(true); + + trial_num = 0; + visual_targets_hit = 0; + audio_targets_hit = 0; + + generate_block(); + } + } + + return PLUGIN_OK; +} + +static int char_width = -1; +static int char_height = -1; + +/** + * Display text. Shamelessly stolen from the star plugin code :D + */ +void star_display_text(char *str, bool waitkey) +{ + int chars_by_line; + int lines_by_screen; + int chars_for_line; + int current_line = 0; + int first_char_index = 0; + char *ptr_char; + char *ptr_line; + int i; + char line[255]; + int key; + bool go_on; + + rb->lcd_getstringsize("a", &char_width, &char_height); + + rb->lcd_clear_display(); + + chars_by_line = LCD_WIDTH / char_width; + lines_by_screen = LCD_HEIGHT / char_height; + + do + { + ptr_char = str + first_char_index; + chars_for_line = 0; + i = 0; + ptr_line = line; + while (i < chars_by_line) + { + switch (*ptr_char) + { + case '\t': + case ' ': + *(ptr_line++) = ' '; + case '\n': + case '\0': + chars_for_line = i; + break; + + default: + *(ptr_line++) = *ptr_char; + } + if (*ptr_char == '\n' || *ptr_char == '\0') + break; + ptr_char++; + i++; + } + + if (chars_for_line == 0) + chars_for_line = i; + + line[chars_for_line] = '\0'; + + /* test if we have cut a word. If it is the case we don't have to */ + /* skip the space */ + if (i == chars_by_line && chars_for_line == chars_by_line) + first_char_index += chars_for_line; + else + first_char_index += chars_for_line + 1; + + /* print the line on the screen */ + rb->lcd_putsxy(0, current_line * char_height, line); + + /* if the number of line showed on the screen is equals to the */ + /* maximum number of line we can show, we wait for a key pressed to */ + /* clear and show the remaining text. */ + current_line++; + if (current_line == lines_by_screen || *ptr_char == '\0') + { + current_line = 0; + rb->lcd_update(); + go_on = false; + while (waitkey && !go_on) + { + key = rb->button_get(true); + switch (key) + { + case NBACK_QUIT: + case BUTTON_LEFT: + case BUTTON_DOWN: + go_on = true; + break; + + default: + if (rb->default_event_handler(key) == SYS_USB_CONNECTED) + { + + usb_detected = true; + go_on = true; + break; + } + } + } + rb->lcd_clear_display(); + } + } while (*ptr_char != '\0'); +} + +enum plugin_status nback_menu() +{ + int selection; + bool menu_quit = false; + struct viewport vp[NB_SCREENS]; + int i; + + MENUITEM_STRINGLIST(menu, "DualNBack Menu", NULL, "Play", "Information", "Quit"); + + FOR_NB_SCREENS(i) + { + rb->viewport_set_defaults(&vp[i], i); + /* we are hiding the statusbar so fix the height also */ + vp[i].y = 0; + vp[i].height = rb->screens[i]->lcdheight; +#if LCD_DEPTH > 1 + if (rb->screens[i]->depth > 1) + { + vp->bg_pattern = LCD_BLACK; + vp->fg_pattern = LCD_WHITE; + } +#endif + } + while(!menu_quit) + { + switch(rb->do_menu(&menu, &selection, vp, true)) + { + case 0: /* Play */ + menu_quit = true; + break; + case 1: /* Information */ + star_display_text( + "INFO\n\n" + "A stimulus consists of a box appearing " + "and a letter being spoken. If two stimuli " + "that are N appearances apart (starting at " + "N=2) have the box in the same position, " + "press left, if the letter is the same, " + "press right. Both may happen. If you do " + "well enough, you will graduate to N=3, etc.", true); + break; + default: /* Quit */ + menu_quit = true; + break; + } + } + + if(selection == 0) { + return nback_game(); + } + + return PLUGIN_OK; +} + +enum plugin_status plugin_start(const struct plugin_api* api, const void* parameter) +{ + /* if you don't use the parameter, you can do like + this to avoid the compiler warning about it */ + (void)parameter; + + /* if you are using a global api pointer, don't forget to copy it! + otherwise you will get lovely "I04: IllInstr" errors... :-) */ + rb = api; + + rb->debugf("Starting up\n"); + + rb->srand (*rb->current_tick); + + screen_width = rb->screens[SCREEN_MAIN]->getwidth(); + screen_height = rb->screens[SCREEN_MAIN]->getheight(); + + rb->lcd_set_backdrop(NULL); + rb->lcd_set_background(LCD_RGBPACK(0,0,0)); + rb->lcd_clear_display(); + + rb->lcd_update(); + + rb->pcm_set_frequency(44100); + + rb->debugf("Generating block...\n"); + + return nback_menu(); +} +