]> git.cworth.org Git - wordgame/blobdiff - rack-fancy.c
Increase the window size a bit
[wordgame] / rack-fancy.c
index 9173abb30143579a73febd69a7a86603a5674246..c764677e6599eab7ff9dc6cd75e5f21f008333a3 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * Copyright © 2006 Carl Worth
  *
- * This program is free software; you can redistribute it and\/or modify
+ * 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)
  * any later version.
 #include <sys/time.h>
 #include <time.h>
 #include <ctype.h>
+#include <math.h>
+#include <gdk/gdkkeysyms.h>
 
 #include "word-game.h"
 #include "demo-item.h"
 
-static gboolean
-on_delete_event (GtkWidget *window,
-                GdkEvent  *event,
-                gpointer   unused_data)
+#define RACK_DICT_ENTRY_OBSCURE        (1<<1)
+#define RACK_DICT_ENTRY_FOUND  (1<<2)
+#define MAX_TILES 7
+
+typedef struct _tile
 {
-    exit (0);
+    char letter;
+    int rack_index;
+    int x, y;
+    GooCanvasItem *item;
+    gboolean guessed;
+} tile_t;
+
+typedef struct _rack
+{
+    tile_t *tiles[MAX_TILES];
+    int num_tiles;
+    char guess[MAX_TILES+1];
+    int guess_length;
+    bag_t bag;
+    dict_t dict;
+    dict_t obscure;
+    dict_t solution;
+    int solution_total;
+    GooCanvasItem *solution_item;
+    gboolean done;
+} rack_t;
+
+#define LETTER_SIZE 60
+#define LETTER_PAD 5
+
+static void
+guess_tile_position (int i, int *x, int *y)
+{
+    *x = 20 + i * (LETTER_SIZE + LETTER_PAD);
+    *y = LETTER_PAD;
 }
 
-static GtkWidget *
-create_window (void)
+static void
+rack_tile_position (int i, int *x, int *y)
 {
-    GtkWidget *window, *scrolled_window;
+    guess_tile_position (i, x, y);
+    *y += (LETTER_SIZE + LETTER_PAD);
+}
 
-    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
-    gtk_window_set_default_size (GTK_WINDOW (window), 500, 500);
-    gtk_widget_show (window);
-    g_signal_connect (window, "delete_event",
-                     (GtkSignalFunc) on_delete_event, NULL);
+typedef enum dict_paint_cursor_show
+{
+    DICT_PAINT_CURSOR_SHOW_FOUND,
+    DICT_PAINT_CURSOR_SHOW_UNFOUND_BLANKS,
+    DICT_PAINT_CURSOR_SHOW_ALL
+} dict_paint_cursor_show_t;
 
-    scrolled_window = gtk_scrolled_window_new (NULL, NULL);
-    gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window),
-                                        GTK_SHADOW_IN);
-    gtk_scrolled_window_set_policy  (GTK_SCROLLED_WINDOW (scrolled_window),
-                                    GTK_POLICY_AUTOMATIC,
-                                    GTK_POLICY_AUTOMATIC);
-    gtk_widget_show (scrolled_window);
-    gtk_container_add (GTK_CONTAINER (window), scrolled_window);
+typedef struct _dict_paint_cursor
+{
+    cairo_t *cr;
+    int line_height;
+    int x;
+    int y;
+    int max_column_width;
+    int max_y;
+    dict_paint_cursor_show_t show;
+} dict_paint_cursor_t;
 
-    return scrolled_window;
+static void
+dict_paint_action (void *closure, char *word, dict_entry_t *entry)
+{
+    dict_paint_cursor_t *cursor = closure;
+    cairo_t *cr = cursor->cr;
+    double new_x, new_y;
+    int found, show_blanks = FALSE;
+
+    if (strlen (word) < 5)
+       return;
+
+    found = *entry & RACK_DICT_ENTRY_FOUND;
+
+    cairo_set_source_rgb (cr, 0, 0, 0); /* black */
+
+    switch (cursor->show) {
+    case DICT_PAINT_CURSOR_SHOW_FOUND:
+       if (! found)
+           return;
+       break;
+    case DICT_PAINT_CURSOR_SHOW_UNFOUND_BLANKS:
+       if (found)
+           return;
+       show_blanks = TRUE;
+       break;
+    case DICT_PAINT_CURSOR_SHOW_ALL:
+       if (! found)
+           cairo_set_source_rgb (cr, 1, 0, 0); /* red */
+       break;
+    }
+
+    /* "Obscure" words get some special coloring. */
+    if (*entry & RACK_DICT_ENTRY_OBSCURE) {
+       if (found) {
+           cairo_set_source_rgb (cr, 0, 1, 0); /* green */
+       } else {
+           if (cursor->show == DICT_PAINT_CURSOR_SHOW_ALL)
+               cairo_set_source_rgb (cr, 0, 0, 1); /* blue */
+           else
+               return;
+       }
+    }
+
+    cairo_move_to (cr, cursor->x, cursor->y);
+    if (show_blanks) {
+       int i, length = strlen (word);
+       for (i = 0; i < length; i++)
+           cairo_show_text (cr, "_");
+    } else {
+       cairo_show_text (cr, word);
+    }
+    cairo_get_current_point (cr, &new_x, &new_y);
+    if (new_x > cursor->max_column_width)
+       cursor->max_column_width = new_x;
+    cursor->y += cursor->line_height;
+    if (cursor->y > cursor->max_y) {
+       cursor->x = cursor->max_column_width + cursor->line_height / 2;
+       cursor->y = cursor->line_height;
+    }
 }
 
-#define MAX_LETTERS 7
-static GooCanvasItem *letter_items[MAX_LETTERS];
-static int num_letters;
+#define SOLUTION_FONT_SIZE     12
+#define SOLUTION_LINE_HEIGHT   (1.5 * SOLUTION_FONT_SIZE)
+
+static void
+dict_paint (cairo_t *cr, void *closure, double width, double height)
+{
+    rack_t *rack = closure;
+    dict_paint_cursor_t cursor;
+    int length, count;
+
+    cairo_save (cr);
+    cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
+
+    cursor.cr = cr;
+
+    cairo_select_font_face (cr, "mono", 0, 0);
+    cairo_set_font_size (cr, SOLUTION_FONT_SIZE);
+    cursor.line_height = SOLUTION_LINE_HEIGHT;
+    
+    cursor.x = 0;
+    cursor.y = cursor.line_height;
+
+    cursor.max_column_width = 0;
+    cursor.max_y = height;
+
+    length = 1;
+    count = 0;
+    do {
+       if (rack->done)
+           cursor.show = DICT_PAINT_CURSOR_SHOW_ALL;
+       else
+           cursor.show = DICT_PAINT_CURSOR_SHOW_FOUND;
+       count += dict_for_each_of_length (&rack->solution,
+                                         dict_paint_action, &cursor,
+                                         length, length);
+       if (! rack->done) {
+           cursor.show = DICT_PAINT_CURSOR_SHOW_UNFOUND_BLANKS;
+           dict_for_each_of_length (&rack->solution,
+                                    dict_paint_action, &cursor,
+                                    length, length);
+       }
+       length++;
+    } while (count < rack->solution_total);
+
+    cairo_restore (cr);
+}
+
+static void
+tile_paint (cairo_t *cr, void *closure, double width, double height)
+{
+    tile_t *tile = closure;
+
+    cairo_pattern_t *gradient;
+    cairo_text_extents_t extents;
+    int rad = (int) MIN (width / 2, height / 2);
+    int cx = width / 2;
+    int cy = cx;
+    int tx, ty;
+    double spot_angle = M_PI / 4.0;
+    double spot_rad = rad / 2.0;
+    char string[2];
+
+    cairo_save (cr);
+
+    gradient = cairo_pattern_create_radial (cx - spot_rad * cos (spot_angle),
+                                           cy - spot_rad * sin (spot_angle),
+                                           0.0,
+                                           cx - spot_rad * cos (spot_angle),
+                                           cy - spot_rad * sin (spot_angle),
+                                           rad + spot_rad);
+    cairo_pattern_add_color_stop_rgb (gradient, 0.0, 1.0, 1.0, 1.0);
+    cairo_pattern_add_color_stop_rgb (gradient, 1.0, 0.33, 0.33, 0.33);
+
+    cairo_set_source (cr, gradient);
+
+    cairo_arc (cr,
+              cx, cy,
+              rad, 0, 2 * M_PI);
+
+    cairo_fill (cr);
+
+    cairo_select_font_face (cr, "mono",
+                           CAIRO_FONT_SLANT_NORMAL,
+                           CAIRO_FONT_WEIGHT_BOLD);
+    cairo_set_font_size (cr, 1.8 * rad);
+
+    string[0] = tile->letter;
+    string[1] = '\0';
+    cairo_text_extents (cr, string, &extents);
+    tx = cx - extents.width / 2 - extents.x_bearing;
+    ty = cy - extents.height / 2 - extents.y_bearing;
+
+    cairo_set_source_rgb (cr, 0.7, 0.7, 0.7);
+    cairo_move_to (cr, tx + 1, ty + 1);
+    cairo_show_text (cr, string);
+               
+    cairo_set_source_rgb (cr, 0.33, 0.33, 0.33);
+    cairo_move_to (cr, tx - 1, ty - 1);
+    cairo_show_text (cr, string);
+
+    cairo_set_source_rgb (cr, 0.2, 0.3, 0.8);
+    cairo_move_to (cr, tx, ty);
+    cairo_show_text (cr, string);
+
+    cairo_restore (cr);
+}
+
+static void
+tile_glide_to (tile_t *tile, int x, int y)
+{
+    goo_canvas_item_animate (tile->item, x, y,
+                            1.0, 0,
+                            500, 40,
+                            GOO_CANVAS_ANIMATE_FREEZE);
+    tile->x = x;
+    tile->y = y;
+}
+
+static tile_t *
+tile_create (GooCanvasItem *parent,
+            char letter, int rack_index)
+{
+    tile_t *tile;
+
+    tile = g_malloc (sizeof (tile_t));
+    tile->letter = tolower (letter);
+    tile->rack_index = rack_index;
+    rack_tile_position (rack_index, &tile->x, &tile->y);
+    tile->item = goo_demo_item_new (parent,
+                                   tile->x, tile->y,
+                                   LETTER_SIZE, LETTER_SIZE,
+                                   tile_paint,
+                                   tile, NULL);
+
+    tile->guessed = FALSE;
+
+    return tile;
+}
+
+static gboolean
+on_delete_event (GtkWidget *window,
+                GdkEvent  *event,
+                gpointer   unused_data)
+{
+    exit (0);
+}
 
 static int
 rand_within (int num_values)
@@ -80,100 +318,317 @@ shuffle (int *array, int length)
     }
 }
 
-#define LETTER_SIZE 60
-#define LETTER_PAD 5
+static void
+rack_init (rack_t      *rack,
+          GooCanvas    *canvas)
+{
+    int i;
+    GooCanvasItem *root = goo_canvas_get_root_item (canvas);
+
+    for (i = 0; i < MAX_TILES; i++)
+       rack->tiles[i] = NULL;
+    rack->num_tiles = 0;
+
+    rack->guess_length = 0;
+    rack->done = FALSE;
+
+    bag_init (&rack->bag);
+
+    dict_init (&rack->dict);
+    dict_add_words_from_file (&rack->dict, "words.txt");
+
+    dict_init (&rack->obscure);
+    dict_add_words_from_file (&rack->obscure, "obscure.txt");
+
+    dict_init (&rack->solution);
+    rack->solution_total = 0;
+
+    for (i = 0; i < MAX_TILES; i++)
+       rack->tiles[i] = tile_create (root, 'A', i);
+    rack->num_tiles = 0;
+}
 
 static void
-get_letter_position (int i, int *x, int *y)
+_flag_obscure_word (void *closure, char *word, dict_entry_t *entry)
 {
-    *x = 20 + i * (LETTER_SIZE + LETTER_PAD);
-    *y = 20;
+    dict_t *obscure = closure;
+    dict_entry_t *obscure_entry;
+
+    obscure_entry = dict_lookup (obscure, word);
+    if (DICT_ENTRY_IS_WORD (obscure_entry))
+       *entry |= RACK_DICT_ENTRY_OBSCURE;
+}
+
+static void
+_at_least_one_is_not_obscure (void *closure, char *word, dict_entry_t *entry)
+{
+    int *result = closure;
+
+    if ((*entry & RACK_DICT_ENTRY_OBSCURE) == 0)
+       *result = 1;
+}
+
+static void
+rack_new_game (rack_t *rack)
+{
+    int i, bottom;
+    char word[MAX_TILES + 1];
+    int length = MAX_TILES;
+    int has_full_length_non_obscure_word;
+
+    /* We'll shuffle as many times as necessary until we can find a
+     * sequence of <length> letters with at least one full-length
+     * word. */
+    while (1) {
+       bag_shuffle (&rack->bag);
+
+       /* In this game, we're not interested in blank tiles, so first
+        * find any blanks and sort them to the bottom of the bag. */
+       i = 0;
+       bottom = BAG_SIZE - 1;
+       for (i = 0; i < bottom; i++) {
+           if (rack->bag.tiles[i] == '?') {
+               rack->bag.tiles[i] = rack->bag.tiles[bottom];
+               rack->bag.tiles[bottom] = '?';
+               bottom--;
+               /* Re-examine ith element */
+               i--;
+           }
+       }
+
+       /* Look at each successive run of tiles in the bag until
+        * finding one that has at least one non-obscure word using
+        * all of its leters. */
+       for (i = 0; i + length <= bottom + 1; i++) {
+           memcpy (word, &rack->bag.tiles[i], length);
+           word[length] = '\0';
+           dict_fini (&rack->solution);
+           dict_init (&rack->solution);
+           subanagram_expand (word, &rack->dict, &rack->solution);
+           dict_for_each (&rack->solution,
+                          _flag_obscure_word, &rack->obscure);
+           has_full_length_non_obscure_word = 0;
+           dict_for_each_of_length (&rack->solution,
+                                    _at_least_one_is_not_obscure,
+                                    &has_full_length_non_obscure_word,
+                                    length, length);
+           if (has_full_length_non_obscure_word)
+               goto DONE;
+           i++;
+       }
+    }
+
+  DONE:
+    rack->solution_total = dict_count (&rack->solution);
+    goo_canvas_item_simple_changed (GOO_CANVAS_ITEM_SIMPLE (rack->solution_item), FALSE);
+
+    for (i = 0; i < length; i++) {
+       rack->tiles[i]->letter = toupper (word[i]);
+       goo_canvas_item_simple_changed (GOO_CANVAS_ITEM_SIMPLE (rack->tiles[i]->item), FALSE);
+    }
+    rack->num_tiles = length;
 }
 
 static gboolean
-on_button_press (GooCanvasItem  *item,
-                GooCanvasItem  *target,
-                GdkEventButton *event,
-                gpointer        data)
+rack_shuffle (rack_t *rack)
 {
-    int indices[MAX_LETTERS];
+    int indices[MAX_TILES];
     int i, x, y;
 
-    for (i = 0; i < num_letters; i++)
+    for (i = 0; i < rack->num_tiles; i++)
        indices[i] = i;
 
-    shuffle (indices, num_letters);
+    shuffle (indices, rack->num_tiles);
 
-    for (i = 0; i < num_letters; i++) {
-       get_letter_position (indices[i], &x, &y);
-       goo_canvas_item_animate (letter_items[i],
-                                x - GOO_DEMO_ITEM (letter_items[i])->x,
-                                y - GOO_DEMO_ITEM (letter_items[i])->y,
-                                1.0, 0,
-                                1000, 40,
-                                GOO_CANVAS_ANIMATE_FREEZE);
+    for (i = 0; i < rack->num_tiles; i++) {
+       rack->tiles[i]->rack_index = indices[i];
+       rack_tile_position (indices[i], &x, &y);
+       tile_glide_to (rack->tiles[i], x, y);
     }
 
     return TRUE;
 }
 
 static void
-create_canvas (GtkWidget *parent, char *word)
+rack_return_tile (rack_t *rack, tile_t *tile)
+{
+    int x, y;
+
+    rack_tile_position (tile->rack_index, &x, &y);
+    tile_glide_to (tile, x, y);
+    tile->guessed = FALSE;
+    rack->guess_length--;
+    rack->guess[rack->guess_length] = '\0';
+}
+
+static void
+rack_return_all (rack_t *rack)
+{
+    int i;
+
+    for (i = 0; i < rack->num_tiles; i++) {
+       if (rack->tiles[i]->guessed)
+           rack_return_tile (rack, rack->tiles[i]);
+    }
+    rack->guess_length = 0;
+    rack->guess[0] = '\0';
+}
+
+static gboolean
+on_key_press_event (GtkWidget  *widget,
+                   GdkEventKey *event,
+                   gpointer     user_data)
+{
+    int i, x, y;
+    char guess_letter;
+    rack_t *rack = user_data;
+
+    if (event->state & GDK_CONTROL_MASK &&
+       event->keyval == GDK_c)
+    {
+       rack->done = TRUE;
+       goo_canvas_item_simple_changed (GOO_CANVAS_ITEM_SIMPLE (rack->solution_item), FALSE);
+    }
+
+    if (event->keyval == GDK_Return) {
+       dict_entry_t *entry;
+       if (rack->done) {
+           rack->done = FALSE;
+           rack_new_game (rack);
+           return TRUE;
+       }
+       if (rack->guess_length >= 3) {
+           entry = dict_lookup (&rack->solution, rack->guess);
+           if (DICT_ENTRY_IS_WORD (entry)) {
+               *entry = *entry | RACK_DICT_ENTRY_FOUND;
+               goo_canvas_item_simple_changed (GOO_CANVAS_ITEM_SIMPLE (rack->solution_item), FALSE);
+           } else {
+               printf ("\a");
+               fflush (stdout);
+           }
+       }
+       rack_return_all (rack);
+       return TRUE;
+    }
+
+    if (event->keyval == GDK_space) {
+       rack_return_all (rack);
+       rack_shuffle (rack);
+       return TRUE;
+    }
+
+    if (event->keyval == GDK_BackSpace) {
+       gboolean found = FALSE;
+       int found_index;
+       x = 0;
+       for (i = 0; i < rack->num_tiles; i++) {
+           /* XXX: evil stuff here... please refactor a lot */
+           if (rack->guess[rack->guess_length-1] == rack->tiles[i]->letter &&
+               rack->tiles[i]->guessed &&
+               rack->tiles[i]->x > x)
+           {
+               found = TRUE;
+               found_index = i;
+           }
+       }
+       if (found) {
+           rack_return_tile (rack, rack->tiles[found_index]);
+           return TRUE;
+       }
+       return FALSE;
+    }
+
+    /* XXX: event->string is deprecated, but the non-deprecated
+     * input-method stuff (GtkIMContext) is extremely non-obvious to
+     * use. */
+    guess_letter = toupper (event->string[0]);
+    for (i = 0; i < rack->num_tiles; i++) {
+       if (guess_letter == rack->tiles[i]->letter && 
+           ! rack->tiles[i]->guessed)
+       {
+           guess_tile_position (rack->guess_length, &x, &y);
+           tile_glide_to (rack->tiles[i], x, y);
+           rack->tiles[i]->guessed = TRUE;
+           rack->guess[rack->guess_length++] = guess_letter;
+           rack->guess[rack->guess_length] = '\0';
+           return TRUE;
+       }
+    }
+
+    return FALSE;
+}
+
+static GtkWidget *
+create_window (rack_t *rack)
+{
+    GtkWidget *window, *scrolled_window;
+
+    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+    gtk_window_set_default_size (GTK_WINDOW (window), 490, 490);
+    gtk_widget_show (window);
+    g_signal_connect (window, "delete_event",
+                     (GtkSignalFunc) on_delete_event, NULL);
+
+    gtk_widget_add_events (window, GDK_KEY_PRESS_MASK);
+    g_signal_connect (window, "key_press_event",
+                     (GtkSignalFunc) on_key_press_event, rack);
+
+    scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+    gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window),
+                                        GTK_SHADOW_IN);
+    gtk_scrolled_window_set_policy  (GTK_SCROLLED_WINDOW (scrolled_window),
+                                    GTK_POLICY_AUTOMATIC,
+                                    GTK_POLICY_AUTOMATIC);
+    gtk_widget_show (scrolled_window);
+    gtk_container_add (GTK_CONTAINER (window), scrolled_window);
+
+    return scrolled_window;
+}
+
+static GooCanvas *
+create_canvas (GtkWidget *parent, rack_t *rack)
 {
     GtkWidget *canvas;
     GooCanvasItem *root;
-    int i;
-    int x, y;
 
     canvas = goo_canvas_new ();
-    gtk_widget_set_size_request (canvas, 400, 400);
-    goo_canvas_set_bounds (GOO_CANVAS (canvas), 0, 0, 400, 400);
+    gtk_widget_set_size_request (canvas, 460, 460);
+    goo_canvas_set_bounds (GOO_CANVAS (canvas), 0, 0, 460, 460);
     gtk_widget_show (canvas);
     gtk_container_add (GTK_CONTAINER (parent), canvas);
 
     root = goo_canvas_get_root_item (GOO_CANVAS (canvas));
 
-    for (i = 0; i < MIN (MAX_LETTERS, strlen (word)); i++) {
-       get_letter_position (i, &x, &y);
-       letter_items[i] = goo_demo_item_new (root,
-                                            x, y,
-                                            LETTER_SIZE, LETTER_SIZE,
-                                            word[i],
+    rack->solution_item = goo_demo_item_new (root,
+                                            20,
+                                            LETTER_PAD + 2 * (LETTER_SIZE + 2 * LETTER_PAD),
+                                            460 - 20, 460 - (LETTER_PAD + 2 * (LETTER_SIZE + 2 * LETTER_PAD)),
+                                            dict_paint, rack,
                                             NULL);
 
-       g_signal_connect (letter_items[i], "button_press_event",
-                         (GtkSignalFunc) on_button_press, NULL);
-    }
-    num_letters = i;
-    while (i < MAX_LETTERS)
-       letter_items[i] = NULL; 
+    return GOO_CANVAS (canvas);
 }
 
 int
 main (int argc, char *argv[])
 {
     struct timeval tv;
-    bag_t bag;
-    char rack[8];
-    int i;
+    rack_t rack;
     GtkWidget *window;
+    GooCanvas *canvas;
 
     gettimeofday (&tv, NULL);
     srand (tv.tv_sec ^ tv.tv_usec);
 
-    bag_init (&bag);
-    bag_shuffle (&bag);
+    gtk_init (&argc, &argv);
 
-    memcpy (rack, bag.tiles, 7);
-    rack[7] = '\0';
+    window = create_window (&rack);
 
-    for (i = 0; i < 7; i++)
-       rack[i] = toupper (rack[i]);
+    canvas = create_canvas (window, &rack);
 
-    gtk_init (&argc, &argv);
-    window = create_window ();
+    rack_init (&rack, canvas);
 
-    create_canvas (window, rack);
+    rack_new_game (&rack);
 
     gtk_main ();