/* gcompris - wordsgame.c * * Copyright (C) 2000, 2008 Bruno Coudoin * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include "gcompris/gcompris.h" #include "gcompris/gameutil.h" #define SOUNDLISTFILE PACKAGE #define MAXWORDSLENGTH 50 static GcomprisWordlist *gc_wordlist = NULL; #if GLIB_CHECK_VERSION(2, 31, 0) static GMutex items_lock; /* No init needed for static GMutexes */ #else GStaticMutex items_lock = G_STATIC_MUTEX_INIT; #endif /* word - word to type overword - part of word allready typed count - number of allready typed letters in word pos - pointer to current position in word letter - current expected letter to type */ typedef struct { GooCanvasItem *rootitem; GooCanvasItem *overwriteItem; gchar *word; gchar *overword; gint count; gchar *pos; gchar *letter; } LettersItem; /* items - array of displayed items items2del - array of items where moved offscreen item_on_focus - item on focus in array items. NULL - not set. */ static GPtrArray *items=NULL; static GPtrArray *items2del=NULL; static LettersItem *item_on_focus=NULL; static GcomprisBoard *gcomprisBoard = NULL; static gint dummy_id = 0; static gint drop_items_id = 0; static gboolean uppercase_only; static void start_board (GcomprisBoard *agcomprisBoard); static void pause_board (gboolean pause); static void end_board (void); static gboolean is_our_board (GcomprisBoard *gcomprisBoard); static void set_level (guint level); static gint key_press(guint keyval, gchar *commit_str, gchar *preedit_str); static GooCanvasItem *wordsgame_create_item(GooCanvasItem *parent); static gint wordsgame_drop_items (GtkWidget *widget, gpointer data); static gint wordsgame_move_items (GtkWidget *widget, gpointer data); static void wordsgame_destroy_item(LettersItem *item); static gboolean wordsgame_delete_items(gpointer user_data); static void wordsgame_destroy_all_items(void); static void wordsgame_next_level(void); static void wordsgame_add_new_item(void); static void wordsgame_config_start(GcomprisBoard *agcomprisBoard, GcomprisProfile *aProfile); static void wordsgame_config_stop(void); static void player_win(LettersItem *item); static void player_loose(void); #define MAX_FALLSPEED 7000 #define MAX_SPEED 150 #define MIN_FALLSPEED 3000 #define MIN_SPEED 50 #define DEFAULT_FALLSPEED 7000 #define DEFAULT_SPEED 150 #define INCREMENT_FALLSPEED 1000 #define INCREMENT_SPEED 10 static guint32 fallSpeed = 0; static double speed = 0.0; static GooCanvasItem *preedit_text = NULL; /* Description of this plugin */ static BoardPlugin menu_bp = { NULL, NULL, N_("Falling Words"), N_("Type the falling words before they reach the ground"), "Bruno Coudoin ", NULL, NULL, NULL, NULL, start_board, pause_board, end_board, is_our_board, key_press, NULL, set_level, NULL, NULL, wordsgame_config_start, wordsgame_config_stop }; /* * Main entry point mandatory for each Gcompris's game * --------------------------------------------------- * */ GET_BPLUGIN_INFO(wordsgame) /* * in : boolean TRUE = PAUSE : FALSE = UNPAUSE * */ static void pause_board (gboolean pause) { if(gcomprisBoard==NULL) return; if(pause) { if (dummy_id) { g_source_remove (dummy_id); dummy_id = 0; } if (drop_items_id) { g_source_remove (drop_items_id); drop_items_id = 0; } } else { if(!drop_items_id) { drop_items_id = g_timeout_add (0, (GSourceFunc) wordsgame_drop_items, NULL); } if(!dummy_id) { dummy_id = g_timeout_add (10, (GSourceFunc) wordsgame_move_items, NULL); } } } /* */ static void start_board (GcomprisBoard *agcomprisBoard) { if(agcomprisBoard!=NULL) { gcomprisBoard=agcomprisBoard; GHashTable *config = gc_db_get_board_conf(); gc_locale_set(g_hash_table_lookup( config, "locale")); gchar *up_init_str = g_hash_table_lookup( config, "uppercase_only"); if (up_init_str && (strcmp(up_init_str, "True")==0)) uppercase_only = TRUE; else uppercase_only = FALSE; g_hash_table_destroy(config); /* disable im_context */ //gcomprisBoard->disable_im_context = TRUE; gc_set_background(goo_canvas_get_root_item(gcomprisBoard->canvas), "wordsgame/scenery_background.png"); gcomprisBoard->level = 1; gcomprisBoard->maxlevel = 6; gcomprisBoard->sublevel = 0; gc_bar_set(GC_BAR_LEVEL|GC_BAR_CONFIG); /* Default speed */ speed=DEFAULT_SPEED; fallSpeed=DEFAULT_FALLSPEED; gc_wordlist = gc_wordlist_get_from_file("wordsgame/default-$LOCALE.xml"); if(!gc_wordlist) { /* Fallback to english */ gc_wordlist = gc_wordlist_get_from_file("wordsgame/default-en.xml"); if(!gc_wordlist) { gcomprisBoard = NULL; gc_dialog(_("Error: We can't find\na list of words to play this game.\n"), gc_board_end); return; } } wordsgame_next_level(); } } static void end_board () { if(gcomprisBoard!=NULL) { pause_board(TRUE); gc_score_end(); #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_lock (&items_lock); #else g_static_mutex_lock (&items_lock); #endif wordsgame_destroy_all_items(); #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_unlock (&items_lock); #else g_static_mutex_unlock (&items_lock); #endif if (preedit_text){ goo_canvas_item_remove(preedit_text); preedit_text=NULL; } gc_im_reset(); gcomprisBoard = NULL; if (gc_wordlist != NULL){ gc_wordlist_free(gc_wordlist); gc_wordlist = NULL; } } gc_locale_set( NULL ); } static void set_level (guint level) { if(gcomprisBoard!=NULL) { gcomprisBoard->level=level; wordsgame_next_level(); } } static gint key_press(guint keyval, gchar *commit_str, gchar *preedit_str) { gchar *letter; gint i; LettersItem *item; gchar *str; gunichar unichar_letter; gint retval = TRUE; if(!gcomprisBoard) return FALSE; if (keyval){ g_warning("keyval %d", keyval); return TRUE; } if (preedit_str){ g_warning("preedit_str %s", preedit_str); /* show the preedit string on bottom of the window */ GcomprisProperties *properties = gc_prop_get (); gchar *text; PangoAttrList *attrs; gint cursor_pos; gtk_im_context_get_preedit_string (properties->context, &text, &attrs, &cursor_pos); if (!preedit_text) preedit_text = \ goo_canvas_text_new (goo_canvas_get_root_item(gcomprisBoard->canvas), "", BOARDWIDTH/2, BOARDHEIGHT - 100, -1, GTK_ANCHOR_N, "font", gc_skin_font_board_huge_bold, //"fill_color_rgba", 0xba00ffff, NULL); g_object_set (preedit_text, "text", text, "attributes", attrs, NULL); return TRUE; } /* commit str */ g_warning("commit_str %s", commit_str); str = commit_str; #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_lock (&items_lock); #else g_static_mutex_lock (&items_lock); #endif for (i=0; i < g_utf8_strlen(commit_str,-1); i++){ unichar_letter = g_utf8_get_char(str); str = g_utf8_next_char(str); if(!g_unichar_isalnum (unichar_letter)){ player_loose(); retval = FALSE; break; } letter = g_new0(gchar,6); g_unichar_to_utf8 (unichar_letter, letter); /* Force entered letter to the casing we expect * Children is to small to manage the caps lock key for now */ if (uppercase_only) { gchar *old = letter; letter = g_utf8_strup(old, -1); g_free(old); } else { gchar *old = letter; letter = g_utf8_strdown(old, -1); g_free(old); } if(item_on_focus==NULL) { for (i=0;ilen;i++) { item=g_ptr_array_index(items,i); g_assert (item!=NULL); if (strcmp(item->letter,letter)==0) { item_on_focus=item; break; } } } if(item_on_focus!=NULL) { if(strcmp(item_on_focus->letter, letter)==0) { gchar *tmpstr; item_on_focus->count++; g_free(item_on_focus->overword); tmpstr = g_utf8_strndup(item_on_focus->word, item_on_focus->count); /* Add the ZERO WIDTH JOINER to force joined char in Arabic and Hangul * http://en.wikipedia.org/wiki/Zero-width_joiner */ item_on_focus->overword = g_strdup_printf("%s%lc", tmpstr, 0x200D); g_free(tmpstr); g_object_set (item_on_focus->overwriteItem, "text", item_on_focus->overword, NULL); if (item_on_focus->countword,-1)) { g_free(item_on_focus->letter); item_on_focus->letter=g_utf8_strndup(item_on_focus->pos,1); item_on_focus->pos=g_utf8_find_next_char(item_on_focus->pos,NULL); } else { player_win(item_on_focus); item_on_focus=NULL; } } else { /* It is a loose : unselect the word and defocus */ g_free(item_on_focus->overword); item_on_focus->overword=g_strdup(" "); item_on_focus->count=0; g_free(item_on_focus->letter); item_on_focus->letter=g_utf8_strndup(item_on_focus->word,1); item_on_focus->pos=g_utf8_find_next_char(item_on_focus->word,NULL); g_object_set (item_on_focus->overwriteItem, "text", item_on_focus->overword, NULL); item_on_focus=NULL; g_free(letter); player_loose(); break; } } else { /* Anyway kid you clicked on the wrong key */ player_loose(); g_free(letter); break; } g_free(letter); } #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_unlock (&items_lock); #else g_static_mutex_unlock (&items_lock); #endif return retval; } static gboolean is_our_board (GcomprisBoard *gcomprisBoard) { if (gcomprisBoard) { if(g_ascii_strcasecmp(gcomprisBoard->type, "wordsgame")==0) { /* Set the plugin entry */ gcomprisBoard->plugin=&menu_bp; return TRUE; } } return FALSE; } /*-------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------*/ /*-------------------------------------------------------------------------------*/ /* Called with items_lock locked */ static void wordsgame_next_level_unlocked() { gcomprisBoard->number_of_sublevel = 10 + ((gcomprisBoard->level-1) * 5); gc_score_start(SCORESTYLE_NOTE, BOARDWIDTH - 195, BOARDHEIGHT - 30, gcomprisBoard->number_of_sublevel); gc_bar_set_level(gcomprisBoard); gc_score_set(gcomprisBoard->sublevel); wordsgame_destroy_all_items(); if (preedit_text){ goo_canvas_item_remove(preedit_text); preedit_text=NULL; } gc_im_reset(); items=g_ptr_array_new(); items2del=g_ptr_array_new(); /* Increase speed only after 5 levels */ if(gcomprisBoard->level > 5) { gint temp = fallSpeed-gcomprisBoard->level*200; if (temp > MIN_FALLSPEED) fallSpeed=temp; } pause_board(FALSE); } /* set initial values for the next level */ static void wordsgame_next_level() { #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_lock (&items_lock); #else g_static_mutex_lock (&items_lock); #endif wordsgame_next_level_unlocked(); #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_unlock (&items_lock); #else g_static_mutex_unlock (&items_lock); #endif } /* Called with items_lock locked */ static void wordsgame_move_item(LettersItem *item) { GooCanvasBounds bounds; goo_canvas_item_translate(item->rootitem, 0, 2.0); goo_canvas_item_get_bounds (item->rootitem, &bounds); if(bounds.y1>BOARDHEIGHT) { if (item == item_on_focus) item_on_focus = NULL; g_ptr_array_remove (items, item); g_ptr_array_add (items2del, item); g_timeout_add (100,(GSourceFunc) wordsgame_delete_items, NULL); player_loose(); } } /* * This does the moves of the game items on the play canvas * */ static gint wordsgame_move_items (GtkWidget *widget, gpointer data) { g_assert (items!=NULL); gint i; LettersItem *item; #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_lock (&items_lock); #else g_static_mutex_lock (&items_lock); #endif for (i=items->len-1;i>=0;i--) { item=g_ptr_array_index(items,i); wordsgame_move_item(item); } #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_unlock (&items_lock); #else g_static_mutex_unlock (&items_lock); #endif dummy_id = g_timeout_add (gc_timing (speed, items->len), (GSourceFunc) wordsgame_move_items, NULL); return (FALSE); } static void wordsgame_destroy_item(LettersItem *item) { /* The items are freed by player_win */ goo_canvas_item_remove(item->rootitem); g_free(item->word); g_free(item->overword); g_free(item->letter); g_free(item); } /* Destroy items that falls out of the canvas */ static gboolean wordsgame_delete_items(gpointer user_data) { LettersItem *item; #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_lock (&items_lock); #else g_static_mutex_lock (&items_lock); #endif /* items2del may be NULL, as we can get called after wordsgame_destroy_all_items() has been called (since we get called as a timeout handler). */ if (items2del!=NULL){ while (items2del->len>0) { item = g_ptr_array_index(items2del,0); g_ptr_array_remove_index_fast(items2del,0); wordsgame_destroy_item(item); } } #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_unlock (&items_lock); #else g_static_mutex_unlock (&items_lock); #endif return (FALSE); } /* Destroy all the items, called with items_lock locked */ static void wordsgame_destroy_all_items() { LettersItem *item; if (items!=NULL){ while (items->len>0) { item = g_ptr_array_index(items,0); g_ptr_array_remove_index_fast(items,0); wordsgame_destroy_item(item); } g_ptr_array_free (items, TRUE); items=NULL; } if (items2del!=NULL){ while (items2del->len>0) { item = g_ptr_array_index(items2del,0); g_ptr_array_remove_index_fast(items2del,0); wordsgame_destroy_item(item); } g_ptr_array_free (items2del, TRUE); items2del=NULL; } } static GooCanvasItem *wordsgame_create_item(GooCanvasItem *parent) { LettersItem *item; gchar *word = gc_wordlist_random_word_get(gc_wordlist, gcomprisBoard->level); GtkAnchorType direction_anchor = GTK_ANCHOR_NW; if(!word) /* Should display the dialog box here */ return NULL; if (uppercase_only) { gchar *old = word; word = g_utf8_strup(old, -1); g_free(old); } // create and init item item = g_new(LettersItem,1); item->word = word; item->overword=g_strdup(""); item->count=0; item->letter=g_utf8_strndup(item->word,1); item->pos=g_utf8_find_next_char(item->word,NULL); if (pango_unichar_direction(g_utf8_get_char(item->word))) direction_anchor = GTK_ANCHOR_NE; item->rootitem = goo_canvas_group_new (parent, NULL); goo_canvas_item_translate(item->rootitem, 0, -12); /* To 'erase' words, I create 2 times the text item. One is empty now */ /* It will be filled each time the user enters the right key */ goo_canvas_text_new (item->rootitem, item->word, (double) 0, (double) 0, -1, direction_anchor, "font", gc_skin_font_board_huge_bold, "fill_color_rgba", 0xffffffff, NULL); item->overwriteItem = \ goo_canvas_text_new (item->rootitem, item->overword, (double) 0, (double) 0, -1, direction_anchor, "font", gc_skin_font_board_huge_bold, "fill-color_rgba", 0x000000ff, NULL); /*set right x position */ GooCanvasBounds bounds; goo_canvas_item_get_bounds (item->rootitem, &bounds); if(direction_anchor == GTK_ANCHOR_NW) goo_canvas_item_translate (item->rootitem, (g_random_int()%(BOARDWIDTH-(gint)(bounds.x2))), 0); else { double new_x = (double)( g_random_int()%BOARDWIDTH); if ( new_x < -bounds.x1 ) new_x -= bounds.x1; goo_canvas_item_translate (item->rootitem, new_x ,(double) 0); } #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_lock (&items_lock); #else g_static_mutex_lock (&items_lock); #endif g_ptr_array_add(items, item); #if GLIB_CHECK_VERSION(2, 31, 0) g_mutex_unlock (&items_lock); #else g_static_mutex_unlock (&items_lock); #endif return (item->rootitem); } static void wordsgame_add_new_item() { g_assert(gcomprisBoard->canvas!=NULL); wordsgame_create_item(goo_canvas_get_root_item(gcomprisBoard->canvas)); } /* * This is called on a low frequency and is used to drop new items * */ static gint wordsgame_drop_items (GtkWidget *widget, gpointer data) { gc_sound_play_ogg ("sounds/level.wav", NULL); wordsgame_add_new_item(); g_source_remove(drop_items_id); drop_items_id = g_timeout_add (fallSpeed,(GSourceFunc) wordsgame_drop_items, NULL); return (FALSE); } /* Called with items_lock locked */ static void player_win(LettersItem *item) { gc_sound_play_ogg ("sounds/flip.wav", NULL); g_assert(gcomprisBoard!=NULL); gcomprisBoard->sublevel++; gc_score_set(gcomprisBoard->sublevel); g_ptr_array_remove(items,item); g_ptr_array_add(items2del,item); g_object_set (item->rootitem, "visibility", GOO_CANVAS_ITEM_INVISIBLE, NULL); g_timeout_add (500,(GSourceFunc) wordsgame_delete_items, NULL); if(gcomprisBoard->sublevel > gcomprisBoard->number_of_sublevel) { /* Try the next level */ gcomprisBoard->level++; gcomprisBoard->sublevel = 0; if(gcomprisBoard->level>gcomprisBoard->maxlevel) gcomprisBoard->level = gcomprisBoard->maxlevel; wordsgame_next_level_unlocked(); gc_sound_play_ogg ("sounds/bonus.wav", NULL); } else { /* Drop a new item now to speed up the game */ if(items->len==0) { if ((fallSpeed-=INCREMENT_FALLSPEED) < MIN_FALLSPEED) fallSpeed+=INCREMENT_FALLSPEED; if ((speed-=INCREMENT_SPEED) < MIN_SPEED) speed+=INCREMENT_SPEED; if (drop_items_id) { /* Remove pending new item creation to sync the falls */ g_source_remove (drop_items_id); drop_items_id = 0; } if(!drop_items_id) { drop_items_id = g_timeout_add (0, (GSourceFunc) wordsgame_drop_items, NULL); } } } } static void player_loose() { gc_sound_play_ogg ("sounds/crash.wav", NULL); } /* ************************************* */ /* * Configuration * */ /* ************************************* */ /* ======================= */ /* = config_start = */ /* ======================= */ static GcomprisProfile *profile_conf; static GcomprisBoard *board_conf; static void save_table (gpointer key, gpointer value, gpointer user_data) { gc_db_set_board_conf ( profile_conf, board_conf, (gchar *) key, (gchar *) value); } static gboolean conf_ok(GHashTable *table) { if (!table){ if (gcomprisBoard) pause_board(FALSE); return TRUE; } g_hash_table_foreach(table, (GHFunc) save_table, NULL); if (gcomprisBoard){ GHashTable *config; if (profile_conf) config = gc_db_get_board_conf(); else config = table; gc_locale_set(g_hash_table_lookup( config, "locale")); gchar *up_init_str = g_hash_table_lookup( config, "uppercase_only"); if (up_init_str) { if(strcmp(up_init_str, "True")==0) uppercase_only = TRUE; else uppercase_only = FALSE; } if (profile_conf) g_hash_table_destroy(config); wordsgame_next_level(); pause_board(FALSE); } board_conf = NULL; profile_conf = NULL; return TRUE; } static void wordsgame_config_start(GcomprisBoard *agcomprisBoard, GcomprisProfile *aProfile) { GcomprisBoardConf *conf; board_conf = agcomprisBoard; profile_conf = aProfile; if (gcomprisBoard) pause_board(TRUE); gchar *label = g_strdup_printf(_("%s configuration\n for profile %s"), agcomprisBoard->name, aProfile? aProfile->name: ""); conf = gc_board_config_window_display( label, conf_ok); g_free(label); /* init the combo to previously saved value */ GHashTable *config = gc_db_get_conf( profile_conf, board_conf); gchar *locale = g_hash_table_lookup( config, "locale"); gc_board_config_combo_locales(conf, locale); gc_board_config_wordlist(conf, "wordsgame/default-$LOCALE.xml"); /* upper case */ gboolean up_init = FALSE; gchar *up_init_str = g_hash_table_lookup( config, "uppercase_only"); if (up_init_str && (strcmp(up_init_str, "True")==0)) up_init = TRUE; gc_board_config_boolean_box(conf, _("Uppercase only text"), "uppercase_only", up_init); } static void wordsgame_config_stop(void) { }