]> git.cworth.org Git - wordgame/blob - rack-fancy.c
Allow a new game to be started by pressing Enter after Control-C
[wordgame] / rack-fancy.c
1 /*
2  * Copyright © 2006 Carl Worth
3  *
4  * This program is free software; you can redistribute it and\/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2, or (at your option)
7  * any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software Foundation,
16  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."
17  */
18 #include <stdlib.h>
19 #include <string.h>
20 #include <goocanvas.h>
21 #include <sys/time.h>
22 #include <time.h>
23 #include <ctype.h>
24 #include <math.h>
25 #include <gdk/gdkkeysyms.h>
26
27 #include "word-game.h"
28 #include "demo-item.h"
29
30 #define RACK_DICT_ENTRY_FOUND (1<<1)
31 #define MAX_TILES 7
32
33 typedef struct _tile
34 {
35     char letter;
36     int rack_index;
37     int x, y;
38     GooCanvasItem *item;
39     gboolean guessed;
40 } tile_t;
41
42 typedef struct _rack
43 {
44     tile_t *tiles[MAX_TILES];
45     int num_tiles;
46     char guess[MAX_TILES+1];
47     int guess_length;
48     bag_t bag;
49     dict_t dict;
50     dict_t solution;
51     GooCanvasItem *solution_item;
52     gboolean done;
53 } rack_t;
54
55 #define LETTER_SIZE 60
56 #define LETTER_PAD 5
57
58 static void
59 guess_tile_position (int i, int *x, int *y)
60 {
61     *x = 20 + i * (LETTER_SIZE + LETTER_PAD);
62     *y = LETTER_PAD;
63 }
64
65 static void
66 rack_tile_position (int i, int *x, int *y)
67 {
68     guess_tile_position (i, x, y);
69     *y += (LETTER_SIZE + LETTER_PAD);
70 }
71
72 typedef struct _dict_paint_cursor
73 {
74     cairo_t *cr;
75     int line_height;
76     int x;
77     int y;
78     int max_column_width;
79     int max_y;
80     gboolean show_all;
81 } dict_paint_cursor_t;
82
83 static void
84 dict_paint_action (void *closure, char *word, dict_entry_t *entry)
85 {
86     dict_paint_cursor_t *cursor = closure;
87     cairo_t *cr = cursor->cr;
88     double new_x, new_y;
89
90     if (strlen (word) < 3)
91         return;
92
93     cairo_move_to (cr, cursor->x, cursor->y);
94     if (*entry & RACK_DICT_ENTRY_FOUND || cursor->show_all) {
95         if (*entry & RACK_DICT_ENTRY_FOUND)
96             cairo_set_source_rgb (cr, 0, 0, 0); /* black */
97         else
98             cairo_set_source_rgb (cr, 1, 0, 0); /* red */
99         cairo_show_text (cr, word);
100     } else {
101         int i, length = strlen (word);
102         for (i = 0; i < length; i++)
103             cairo_show_text (cr, "_");
104     }
105     cairo_get_current_point (cr, &new_x, &new_y);
106     if (new_x > cursor->max_column_width)
107         cursor->max_column_width = new_x;
108     cursor->y += cursor->line_height;
109     if (cursor->y > cursor->max_y) {
110         cursor->x = cursor->max_column_width + cursor->line_height / 2;
111         cursor->y = cursor->line_height;
112     }
113 }
114
115 #define SOLUTION_FONT_SIZE      12
116 #define SOLUTION_LINE_HEIGHT    (1.5 * SOLUTION_FONT_SIZE)
117
118 static void
119 dict_paint (cairo_t *cr, void *closure, double width, double height)
120 {
121     rack_t *rack = closure;
122     dict_paint_cursor_t cursor;
123
124     cairo_save (cr);
125     cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
126
127     cursor.cr = cr;
128
129     cairo_select_font_face (cr, "mono", 0, 0);
130     cairo_set_font_size (cr, SOLUTION_FONT_SIZE);
131     cursor.line_height = SOLUTION_LINE_HEIGHT;
132     
133     cursor.x = 0;
134     cursor.y = cursor.line_height;
135
136     cursor.max_column_width = 0;
137     cursor.max_y = height;
138
139     cursor.show_all = rack->done;
140
141     dict_for_each_by_length (&rack->solution,
142                              dict_paint_action,
143                              &cursor);
144
145     cairo_restore (cr);
146 }
147
148 static void
149 tile_paint (cairo_t *cr, void *closure, double width, double height)
150 {
151     tile_t *tile = closure;
152
153     cairo_pattern_t *gradient;
154     cairo_text_extents_t extents;
155     int rad = (int) MIN (width / 2, height / 2);
156     int cx = width / 2;
157     int cy = cx;
158     int tx, ty;
159     double spot_angle = M_PI / 4.0;
160     double spot_rad = rad / 2.0;
161     char string[2];
162
163     cairo_save (cr);
164
165     gradient = cairo_pattern_create_radial (cx - spot_rad * cos (spot_angle),
166                                             cy - spot_rad * sin (spot_angle),
167                                             0.0,
168                                             cx - spot_rad * cos (spot_angle),
169                                             cy - spot_rad * sin (spot_angle),
170                                             rad + spot_rad);
171     cairo_pattern_add_color_stop_rgb (gradient, 0.0, 1.0, 1.0, 1.0);
172     cairo_pattern_add_color_stop_rgb (gradient, 1.0, 0.33, 0.33, 0.33);
173
174     cairo_set_source (cr, gradient);
175
176     cairo_arc (cr,
177                cx, cy,
178                rad, 0, 2 * M_PI);
179
180     cairo_fill (cr);
181
182     cairo_select_font_face (cr, "mono",
183                             CAIRO_FONT_SLANT_NORMAL,
184                             CAIRO_FONT_WEIGHT_BOLD);
185     cairo_set_font_size (cr, 1.8 * rad);
186
187     string[0] = tile->letter;
188     string[1] = '\0';
189     cairo_text_extents (cr, string, &extents);
190     tx = cx - extents.width / 2 - extents.x_bearing;
191     ty = cy - extents.height / 2 - extents.y_bearing;
192
193     cairo_set_source_rgb (cr, 0.7, 0.7, 0.7);
194     cairo_move_to (cr, tx + 1, ty + 1);
195     cairo_show_text (cr, string);
196                 
197     cairo_set_source_rgb (cr, 0.33, 0.33, 0.33);
198     cairo_move_to (cr, tx - 1, ty - 1);
199     cairo_show_text (cr, string);
200
201     cairo_set_source_rgb (cr, 0.2, 0.3, 0.8);
202     cairo_move_to (cr, tx, ty);
203     cairo_show_text (cr, string);
204
205     cairo_restore (cr);
206 }
207
208 static void
209 tile_glide_to (tile_t *tile, int x, int y)
210 {
211     goo_canvas_item_animate (tile->item, x, y,
212                              1.0, 0,
213                              500, 40,
214                              GOO_CANVAS_ANIMATE_FREEZE);
215     tile->x = x;
216     tile->y = y;
217 }
218
219 static tile_t *
220 tile_create (GooCanvasItem *parent,
221              char letter, int rack_index)
222 {
223     tile_t *tile;
224
225     tile = g_malloc (sizeof (tile_t));
226     tile->letter = tolower (letter);
227     tile->rack_index = rack_index;
228     rack_tile_position (rack_index, &tile->x, &tile->y);
229     tile->item = goo_demo_item_new (parent,
230                                     tile->x, tile->y,
231                                     LETTER_SIZE, LETTER_SIZE,
232                                     tile_paint,
233                                     tile, NULL);
234
235     tile->guessed = FALSE;
236
237     return tile;
238 }
239
240 static gboolean
241 on_delete_event (GtkWidget *window,
242                  GdkEvent  *event,
243                  gpointer   unused_data)
244 {
245     exit (0);
246 }
247
248 static int
249 rand_within (int num_values)
250 {
251     return (int) ((double) num_values * (rand() / (RAND_MAX + 1.0)));
252 }
253
254 static void
255 shuffle (int *array, int length)
256 {
257     int i, r, tmp;
258
259     for (i = 0; i < length; i++)
260     {
261         r = i + rand_within (length - i);
262         tmp = array[i];
263         array[i] = array[r];
264         array[r] = tmp;
265     }
266 }
267
268 static void
269 rack_init (rack_t       *rack,
270            GooCanvas    *canvas)
271 {
272     int i;
273     GooCanvasItem *root = goo_canvas_get_root_item (canvas);
274
275     for (i = 0; i < MAX_TILES; i++)
276         rack->tiles[i] = NULL;
277     rack->num_tiles = 0;
278
279     rack->guess_length = 0;
280     rack->done = FALSE;
281
282     bag_init (&rack->bag);
283
284     dict_init (&rack->dict);
285     dict_add_words_from_file (&rack->dict, "words.txt");
286
287     dict_init (&rack->solution);
288
289     for (i = 0; i < MAX_TILES; i++)
290         rack->tiles[i] = tile_create (root, 'A', i);
291     rack->num_tiles = 0;
292 }
293
294 static void
295 rack_new_game (rack_t *rack)
296 {
297     int i;
298     char *draw;
299     char word[8];
300
301     /* Clean up any remnants from the last game */
302     dict_fini (&rack->solution);
303
304     bag_shuffle (&rack->bag);
305
306     /* Keep drawing until we get 7 non-blank tiles */
307     i = 0;
308     draw = rack->bag.tiles;
309     while (i < 7) {
310         if (*draw != '?')
311             word[i++] = *draw;
312         draw++;
313     }
314     word[7] = '\0';
315
316     for (i = 0; i < 7; i++) {
317         rack->tiles[i]->letter = toupper (word[i]);
318         goo_canvas_item_simple_changed (GOO_CANVAS_ITEM_SIMPLE (rack->tiles[i]->item), FALSE);
319     }
320     rack->num_tiles = 7;
321
322     dict_init (&rack->solution);
323     subanagram_expand (word, &rack->dict, &rack->solution);
324     goo_canvas_item_simple_changed (GOO_CANVAS_ITEM_SIMPLE (rack->solution_item), FALSE);
325 }
326
327 static gboolean
328 rack_shuffle (rack_t *rack)
329 {
330     int indices[MAX_TILES];
331     int i, x, y;
332
333     for (i = 0; i < rack->num_tiles; i++)
334         indices[i] = i;
335
336     shuffle (indices, rack->num_tiles);
337
338     for (i = 0; i < rack->num_tiles; i++) {
339         rack->tiles[i]->rack_index = indices[i];
340         rack_tile_position (indices[i], &x, &y);
341         tile_glide_to (rack->tiles[i], x, y);
342     }
343
344     return TRUE;
345 }
346
347 static void
348 rack_return_tile (rack_t *rack, tile_t *tile)
349 {
350     int x, y;
351
352     rack_tile_position (tile->rack_index, &x, &y);
353     tile_glide_to (tile, x, y);
354     tile->guessed = FALSE;
355     rack->guess_length--;
356     rack->guess[rack->guess_length] = '\0';
357 }
358
359 static void
360 rack_return_all (rack_t *rack)
361 {
362     int i;
363
364     for (i = 0; i < rack->num_tiles; i++) {
365         if (rack->tiles[i]->guessed)
366             rack_return_tile (rack, rack->tiles[i]);
367     }
368     rack->guess_length = 0;
369     rack->guess[0] = '\0';
370 }
371
372 static gboolean
373 on_key_press_event (GtkWidget   *widget,
374                     GdkEventKey *event,
375                     gpointer     user_data)
376 {
377     int i, x, y;
378     char guess_letter;
379     rack_t *rack = user_data;
380
381     if (event->state & GDK_CONTROL_MASK &&
382         event->keyval == GDK_c)
383     {
384         rack->done = TRUE;
385         goo_canvas_item_simple_changed (GOO_CANVAS_ITEM_SIMPLE (rack->solution_item), FALSE);
386     }
387
388     if (event->keyval == GDK_Return) {
389         dict_entry_t *entry;
390         if (rack->done) {
391             rack->done = FALSE;
392             rack_new_game (rack);
393             return TRUE;
394         }
395         if (rack->guess_length >= 3) {
396             entry = dict_lookup (&rack->solution, rack->guess);
397             if (DICT_ENTRY_IS_WORD (entry)) {
398                 *entry = *entry | RACK_DICT_ENTRY_FOUND;
399                 goo_canvas_item_simple_changed (GOO_CANVAS_ITEM_SIMPLE (rack->solution_item), FALSE);
400             } else {
401                 printf ("\a");
402                 fflush (stdout);
403             }
404         }
405         rack_return_all (rack);
406         return TRUE;
407     }
408
409     if (event->keyval == GDK_space) {
410         rack_return_all (rack);
411         rack_shuffle (rack);
412         return TRUE;
413     }
414
415     if (event->keyval == GDK_BackSpace) {
416         gboolean found = FALSE;
417         int found_index;
418         x = 0;
419         for (i = 0; i < rack->num_tiles; i++) {
420             /* XXX: evil stuff here... please refactor a lot */
421             if (rack->guess[rack->guess_length-1] == rack->tiles[i]->letter &&
422                 rack->tiles[i]->guessed &&
423                 rack->tiles[i]->x > x)
424             {
425                 found = TRUE;
426                 found_index = i;
427             }
428         }
429         if (found) {
430             rack_return_tile (rack, rack->tiles[found_index]);
431             return TRUE;
432         }
433         return FALSE;
434     }
435
436     /* XXX: event->string is deprecated, but the non-deprecated
437      * input-method stuff (GtkIMContext) is extremely non-obvious to
438      * use. */
439     guess_letter = toupper (event->string[0]);
440     for (i = 0; i < rack->num_tiles; i++) {
441         if (guess_letter == rack->tiles[i]->letter && 
442             ! rack->tiles[i]->guessed)
443         {
444             guess_tile_position (rack->guess_length, &x, &y);
445             tile_glide_to (rack->tiles[i], x, y);
446             rack->tiles[i]->guessed = TRUE;
447             rack->guess[rack->guess_length++] = guess_letter;
448             rack->guess[rack->guess_length] = '\0';
449             return TRUE;
450         }
451     }
452
453     return FALSE;
454 }
455
456 static GtkWidget *
457 create_window (rack_t *rack)
458 {
459     GtkWidget *window, *scrolled_window;
460
461     window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
462     gtk_window_set_default_size (GTK_WINDOW (window), 500, 500);
463     gtk_widget_show (window);
464     g_signal_connect (window, "delete_event",
465                       (GtkSignalFunc) on_delete_event, NULL);
466
467     gtk_widget_add_events (window, GDK_KEY_PRESS_MASK);
468     g_signal_connect (window, "key_press_event",
469                       (GtkSignalFunc) on_key_press_event, rack);
470
471     scrolled_window = gtk_scrolled_window_new (NULL, NULL);
472     gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window),
473                                          GTK_SHADOW_IN);
474     gtk_scrolled_window_set_policy  (GTK_SCROLLED_WINDOW (scrolled_window),
475                                      GTK_POLICY_AUTOMATIC,
476                                      GTK_POLICY_AUTOMATIC);
477     gtk_widget_show (scrolled_window);
478     gtk_container_add (GTK_CONTAINER (window), scrolled_window);
479
480     return scrolled_window;
481 }
482
483 static GooCanvas *
484 create_canvas (GtkWidget *parent, rack_t *rack)
485 {
486     GtkWidget *canvas;
487     GooCanvasItem *root;
488
489     canvas = goo_canvas_new ();
490     gtk_widget_set_size_request (canvas, 400, 480);
491     goo_canvas_set_bounds (GOO_CANVAS (canvas), 0, 0, 400, 480);
492     gtk_widget_show (canvas);
493     gtk_container_add (GTK_CONTAINER (parent), canvas);
494
495     root = goo_canvas_get_root_item (GOO_CANVAS (canvas));
496
497     rack->solution_item = goo_demo_item_new (root,
498                                              20,
499                                              LETTER_PAD + 2 * (LETTER_SIZE + 2 * LETTER_PAD),
500                                              400 - 20, 480 - (LETTER_PAD + 2 * (LETTER_SIZE + 2 * LETTER_PAD)),
501                                              dict_paint, rack,
502                                              NULL);
503
504     return GOO_CANVAS (canvas);
505 }
506
507 int
508 main (int argc, char *argv[])
509 {
510     struct timeval tv;
511     rack_t rack;
512     GtkWidget *window;
513     GooCanvas *canvas;
514
515     gettimeofday (&tv, NULL);
516     srand (tv.tv_sec ^ tv.tv_usec);
517
518     gtk_init (&argc, &argv);
519
520     window = create_window (&rack);
521
522     canvas = create_canvas (window, &rack);
523
524     rack_init (&rack, canvas);
525
526     rack_new_game (&rack);
527
528     gtk_main ();
529
530     return 0;
531 }