app menu
[rfk] / rfk.c
1 /*  robotfindskitten for maemo
2  *  original by Leonard Richardson, 1997
3  *  ported to maemo by Thomas Thurman, 2009
4  *  suggestions welcome
5  *  Compile with:
6  *  gcc -Wall -g rfk.c -o rfk `pkg-config --cflags --libs gtk+-2.0 hildon-1 dbus-glib-1 dbus-1`
7  */
8
9 #include <dbus/dbus-glib.h>
10 #include <gtk/gtk.h>
11 #include <stdlib.h>
12 #include <glib.h>
13 #include <hildon/hildon.h>
14 #include <math.h>
15 #include <stdio.h>
16 #include <string.h>
17
18 #define ARENA_WIDTH 25
19 #define ARENA_HEIGHT 12
20
21 const int amount_of_random_stuff = 15;
22
23 typedef enum {
24   STATE_PROLOGUE,
25   STATE_PLAYING,
26   STATE_EPILOGUE,
27   STATE_LAST
28 } StateOfPlay;
29
30 StateOfPlay current_state = STATE_LAST;
31 GtkWidget* state_widget[STATE_LAST];
32
33 GSList *nki = NULL;
34 guint nki_count = 0;
35
36 GtkWidget *arena[ARENA_WIDTH][ARENA_HEIGHT];
37 GtkWidget *window, *robot, *kitten;
38 int robot_x, robot_y;
39 gboolean *used = NULL;
40
41 GdkPixbuf *robot_pic, *love_pic, *kitten_pic;
42
43 const GdkColor black = { 0, };
44
45 /****************************************************************/
46 /* Random object descriptions.                                  */
47 /****************************************************************/
48
49 static char *
50 description (void)
51 {
52   int r;
53    
54   do
55     {
56       r = random() % nki_count;
57     }
58   while (used[r]);
59   used[r] = TRUE;
60
61   return g_slist_nth_data (nki, r);
62 }
63
64 /****************************************************************/
65 /* Placing objects.                                             */
66 /****************************************************************/
67
68 static void
69 place_in_arena_at_xy (GtkWidget *item, int x, int y)
70 {
71   arena[x][y] = item;
72
73   gtk_table_attach_defaults (GTK_TABLE (state_widget[STATE_PLAYING]),
74                              item,
75                              x, x+1,
76                              y, y+1);
77
78   if (item==robot)
79     {
80       robot_x = x;
81       robot_y = y;
82     }
83 }
84
85 static void
86 place_in_arena_randomly (GtkWidget *item)
87 {
88   int x, y;
89    
90   do
91     {
92       x = random() % ARENA_WIDTH;
93       y = random() % ARENA_HEIGHT;
94     }
95   while (arena[x][y]);
96
97   place_in_arena_at_xy (item, x, y);
98 }
99
100 /****************************************************************/
101 /* Labels representing things the robot might find.             */
102 /****************************************************************/
103
104 static GtkWidget *
105 random_character (gchar *description)
106 {
107   gchar character[2] = { random() % ('~'-'!') + '!', 0 };
108   gchar *escaped_character = g_markup_escape_text (character, -1);
109   gchar *markup = g_strdup_printf ("<span color=\"#%02x%02x%02x\">%s</span>",
110                                    (int) (random() % 0x7F)+0x80,
111                                    (int) (random() % 0x7F)+0x80,
112                                    (int) (random() % 0x7F)+0x80,
113                                    escaped_character);
114   GtkWidget *result = gtk_label_new (NULL);
115   gtk_label_set_markup (GTK_LABEL (result), markup);
116   g_free (markup);
117   g_free (escaped_character);
118
119   g_object_set_data (G_OBJECT (result), "examine", description);
120
121   return result;
122 }
123
124 /****************************************************************/
125 /* Talking back to the user.                                    */
126 /****************************************************************/
127
128 static void
129 show_message (const char *message)
130 {
131   HildonNote* note = HILDON_NOTE
132     (hildon_note_new_information (GTK_WINDOW (window),
133                                   message?message:
134                                   "Some message was supposed to be here."));
135   gtk_dialog_run (GTK_DIALOG (note));
136   gtk_widget_destroy (GTK_WIDGET (note));
137 }
138
139 /****************************************************************/
140 /* Loading the non-kitten objects.                              */
141 /****************************************************************/
142
143 static void
144 ensure_messages_loaded (void)
145 {
146   FILE *nki_file = NULL;
147   gchar *line = NULL;
148   gboolean headers = TRUE;
149
150   if (nki_count)
151     return;
152
153   nki_file = fopen ("/usr/share/rfk/non-kitten-items.rfk", "r");
154
155   if (!nki_file)
156     {
157       show_message ("Could not open list of non-kitten items!  Must quit.");
158       exit (EXIT_FAILURE);
159     }
160
161   while (!feof (nki_file))
162     {
163       char newline;
164       if (fscanf (nki_file, "%a[^\n]%c", &line, &newline) == EOF)
165         {
166           break;
167         }
168
169       if (strcmp(line, "")==0)
170         {
171           headers = FALSE;
172           fscanf (nki_file, "%c", &newline); 
173           free (line);
174         }
175       else if (headers)
176         {
177           /* we ignore all the headers for now */
178           free (line);
179         }
180       else
181         {
182           nki = g_slist_prepend (nki, line);
183           nki_count++;
184         }
185     }
186
187   fclose (nki_file);
188 }
189
190 static void
191 load_images (void)
192 {
193   robot_pic = gdk_pixbuf_new_from_file ("/usr/share/rfk/rfk-robot.png", NULL);
194   love_pic = gdk_pixbuf_new_from_file ("/usr/share/rfk/rfk-love.png", NULL);
195   kitten_pic = gdk_pixbuf_new_from_file ("/usr/share/rfk/rfk-kitten.png", NULL);
196 }
197
198 /****************************************************************/
199 /* Stop doing that, and do something else.                      */
200 /****************************************************************/
201
202 static void
203 switch_state (StateOfPlay new_state)
204 {
205   if (current_state != STATE_LAST)
206     {
207       gtk_container_remove (GTK_CONTAINER (window), state_widget[current_state]);
208     }
209   gtk_container_add (GTK_CONTAINER (window), state_widget[new_state]);
210
211   gtk_widget_show_all (window);
212   gdk_window_set_events (GTK_WIDGET (window)->window,
213                          gdk_window_get_events(GTK_WIDGET (window)->window) | GDK_BUTTON_PRESS_MASK);
214
215   current_state = new_state;
216 }
217
218 /****************************************************************/
219 /* Things we need DBus for: online help, and vibration.         */
220 /****************************************************************/
221
222 static void
223 call_dbus (DBusBusType type,
224            char *name,
225            char *path,
226            char *interface,
227            char *method,
228            char *parameter)
229 {
230   DBusGConnection *connection;
231   GError *error = NULL;
232
233   DBusGProxy *proxy;
234
235   connection = dbus_g_bus_get (type,
236                                &error);
237   if (connection == NULL)
238     {
239       show_message (error->message);
240       g_error_free (error);
241       return;
242     }
243
244   proxy = dbus_g_proxy_new_for_name (connection, name, path, interface);
245
246   error = NULL;
247   if (!dbus_g_proxy_call (proxy, method, &error,
248                           G_TYPE_STRING, parameter,
249                           G_TYPE_INVALID,
250                           G_TYPE_INVALID))
251     {
252       show_message (error->message);
253       g_error_free (error);
254     }
255 }
256
257 static gboolean
258 get_help (gpointer button, gpointer data)
259 {
260   call_dbus (DBUS_BUS_SESSION,
261              "com.nokia.osso_browser",
262              "/com/nokia/osso_browser/request",
263              "com.nokia.osso_browser",
264              "load_url",
265              "/usr/share/rfk/help.html");
266   return FALSE;
267 }
268
269 static void
270 vibrate (void)
271 {
272   call_dbus (DBUS_BUS_SYSTEM,
273              "com.nokia.mce",
274              "/com/nokia/mce/request",
275              "com.nokia.mce.request",
276              "req_vibrator_pattern_activate",
277              "PatternIncomingMessage");
278 }
279
280 /****************************************************************/
281 /* The ending animation.                                        */
282 /****************************************************************/
283
284 gboolean animation_running = FALSE;
285
286 static gboolean
287 ending_animation_quit (gpointer data)
288 {
289   switch_state (STATE_PROLOGUE);
290   return FALSE;
291 }
292
293 static gboolean
294 ending_animation_draw (GtkWidget *widget, GdkEventExpose *event, gpointer data)
295 {
296   static int cycle_count = 0;
297
298   static int robot_x = 0;
299   static int robot_stop = 0;
300   static int kitten_x = 0;
301   static int all_y = 0;
302   static GdkGC *gc = NULL;
303
304   const int stepsize = 3;
305   static int love_size = 40;
306
307   if (!kitten_x)
308     {
309       gc = gdk_gc_new (GDK_DRAWABLE (widget->window));
310
311       all_y = (event->area.height - gdk_pixbuf_get_height (love_pic)) / 2;
312
313       robot_stop = gdk_pixbuf_get_width (robot_pic) + gdk_pixbuf_get_width (love_pic);
314       kitten_x = event->area.width - (cycle_count*stepsize + gdk_pixbuf_get_width (kitten_pic));
315     }
316
317   gdk_gc_set_foreground (gc, &black);
318
319   gdk_draw_rectangle (GDK_DRAWABLE (widget->window),
320                       gc,
321                       TRUE,
322                       0, 0, event->area.width, event->area.height);
323
324   gdk_draw_pixbuf (GDK_DRAWABLE (widget->window),
325                    gc,
326                    robot_pic, 0, 0,
327                    robot_x, all_y,
328                    -1, -1,
329                    GDK_RGB_DITHER_NONE, 0, 0);
330
331   gdk_draw_pixbuf (GDK_DRAWABLE (widget->window),
332                    gc,
333                    kitten_pic, 0, 0,
334                    kitten_x, all_y,
335                    -1, -1,
336                    GDK_RGB_DITHER_NONE, 0, 0);
337
338   if (robot_x+robot_stop < kitten_x)
339     {
340       cycle_count++;
341       robot_x += stepsize;
342       kitten_x -= stepsize;
343     }
344   else
345     {
346       GdkPixbuf *scaled_love_pic =
347         gdk_pixbuf_scale_simple (love_pic,
348                                  love_size,
349                                  love_size,
350                                  GDK_INTERP_BILINEAR);
351
352       gdk_draw_pixbuf (GDK_DRAWABLE(widget->window),
353                        gc,
354                        scaled_love_pic, 0, 0,
355                        robot_x + gdk_pixbuf_get_width (robot_pic) +
356                        (gdk_pixbuf_get_width (love_pic)-love_size)/2,
357                        all_y + (gdk_pixbuf_get_height (love_pic)-love_size)/2,
358                        -1, -1,
359                        GDK_RGB_DITHER_NONE, 0, 0);
360
361       love_size += 10;
362
363       if (love_size >= gdk_pixbuf_get_width (love_pic))
364         {
365           /* all done! */
366           
367           vibrate ();
368           
369           animation_running = FALSE;
370
371           g_timeout_add (2000, ending_animation_quit, NULL);
372
373           gdk_gc_unref (gc);
374           love_size = 40;
375           cycle_count = 0;
376           robot_x = 0;
377           robot_stop = 0;
378           kitten_x = 0;
379           all_y = 0;
380           gc = NULL;
381         }
382     }
383
384   return TRUE;
385 }
386
387 static gboolean
388 ending_animation_step (gpointer data)
389 {
390   if (animation_running)
391     {
392       gdk_window_invalidate_rect (state_widget[STATE_EPILOGUE]->window,
393                                   NULL, TRUE);
394
395       return TRUE;
396     }
397   else
398     return FALSE;
399 }
400
401 static void
402 ending_animation ()
403 {
404   if (current_state!=STATE_EPILOGUE)
405     {
406       animation_running = TRUE;
407       g_timeout_add (10, ending_animation_step, NULL);
408     }
409 }
410
411 /****************************************************************/
412 /* Moving the robot.  Way to go, robot!                         */
413 /****************************************************************/
414
415 typedef struct {
416   guint gdk_key;
417   gchar vi_key; /* or nethack equivalent */
418   guint8 move_x;
419   guint8 move_y;
420 } direction;
421
422 direction directions[] = {
423   { GDK_Home,      'y', -1, -1 },
424   { GDK_Left,      'h', -1,  0 },
425   { GDK_End,       'b', -1,  1 },
426   { GDK_Down,      'j',  0,  1 },
427   { GDK_Page_Down, 'n',  1,  1 },
428   { GDK_Right,     'l',  1,  0 },
429   { GDK_Page_Up,   'u',  1, -1 },
430   { GDK_Up,        'k',  0, -1 }
431 };
432
433 static gboolean
434 move_robot (guint8 whichway)
435 {
436   GtkWidget *new_space;
437   gint8 dx = directions[whichway].move_x;
438   gint8 dy = directions[whichway].move_y;
439
440   const char *found;
441
442   if (robot_x+dx<0 ||
443       robot_y+dy<0 ||
444       robot_x+dx>=ARENA_WIDTH ||
445       robot_y+dy>=ARENA_HEIGHT)
446     return TRUE;
447
448   new_space = arena[robot_x+dx][robot_y+dy];
449   found = g_object_get_data (G_OBJECT (new_space), "examine");
450
451   if (found && *found)
452     {
453       show_message (found);
454
455       if (new_space == kitten)
456         {
457           switch_state (STATE_EPILOGUE);
458         }
459
460       return TRUE;
461     }
462   else
463     {
464       /* just an ordinary move into an empty space */
465
466       g_object_ref (new_space);
467
468       gtk_container_remove (GTK_CONTAINER (state_widget[STATE_PLAYING]), robot);
469       gtk_container_remove (GTK_CONTAINER (state_widget[STATE_PLAYING]), new_space);
470
471       place_in_arena_at_xy (new_space, robot_x, robot_y);
472       place_in_arena_at_xy (robot, robot_x+dx, robot_y+dy);
473
474       g_object_unref (new_space);
475
476       return FALSE;
477     }
478 }
479
480 /****************************************************************/
481 /* Event handlers.                                              */
482 /****************************************************************/
483
484 static gboolean
485 on_window_clicked (GtkWidget      *widget,
486                    GdkEventButton *event,
487                    gpointer        user_data)
488 {
489   /** Centre point of robot's representation on screen */
490   int rx, ry;
491   double angle;
492
493   if (current_state!=STATE_PLAYING)
494     {
495       return TRUE;
496     }
497
498   rx = (robot->allocation.x+robot->allocation.width/2);
499   ry = (robot->allocation.y+robot->allocation.height/2);
500
501   angle = atan2(event->x - rx,
502                 event->y - ry) +
503     M_PI * (9/8);
504
505   move_robot (((int) (angle / (M_PI/4))) % 8);
506
507   return TRUE;
508 }
509
510 static gboolean
511 on_key_pressed (GtkWidget      *widget,
512                 GdkEventKey    *event,
513                 gpointer        user_data)
514 {
515   gint i;
516   guint keyval = event->keyval;
517
518   if (current_state!=STATE_PLAYING)
519     {
520       return FALSE;
521     }
522
523   if (keyval>='A' && keyval<='Z')
524     {
525       keyval += ('a'-'A');
526     }
527
528   for (i=0; i<G_N_ELEMENTS(directions); i++)
529     {
530       if (keyval==directions[i].gdk_key ||
531           keyval==directions[i].vi_key)
532         {
533           if (event->state & GDK_SHIFT_MASK)
534             {
535               while (!move_robot (i))
536                 {
537                   /* keep going, robot! */
538                 }
539             }
540           else
541             {
542               move_robot (i);
543             }
544           return FALSE;
545         }
546     }
547
548   if (keyval=='d' && event->state & GDK_CONTROL_MASK)
549     {
550       /* secret debugging key */
551       show_message (gtk_label_get_text (GTK_LABEL (kitten)));
552     }
553
554   return FALSE;
555 }
556
557 static void
558 play_game (gpointer button, gpointer data)
559 {
560   switch_state (STATE_PLAYING);
561 }
562
563 static void
564 restart (gpointer button, gpointer data)
565 {
566   if (current_state == STATE_EPILOGUE)
567     {
568       show_message ("Have patience while robotfindskitten.");
569     }
570   else
571     {
572       switch_state (STATE_PROLOGUE);
573     }
574 }
575
576 static void
577 set_up_board (void)
578 {
579   guint x, y;
580
581   if (current_state==STATE_PLAYING)
582     {
583       /* end of the game; clean up */
584
585       for (x=0; x < ARENA_WIDTH; x++)
586         for (y=0; y < ARENA_HEIGHT; y++)
587           if (arena[x][y])
588             {
589               gtk_container_remove (GTK_CONTAINER (state_widget[STATE_PLAYING]),
590                                     arena[x][y]);
591               arena[x][y] = NULL;
592             }
593
594       g_object_unref (robot);
595       g_object_unref (kitten);
596     }
597   else
598     {
599       /* make everything new */
600   
601       g_free (used);
602       used = g_malloc0 (nki_count * sizeof(gboolean));
603
604       robot = gtk_label_new ("#");
605       g_object_ref (robot);
606       kitten = random_character ("You found kitten!  Way to go, robot!");
607       g_object_ref (kitten);
608
609       place_in_arena_randomly (robot);
610       place_in_arena_randomly (kitten);
611
612       if (nki_count < amount_of_random_stuff)
613         {
614           /* sanity check failed */
615           show_message ("There are too few non-kitten items to play a meaningful game.");
616           exit (EXIT_FAILURE);
617         }
618
619       for (x=0; x < amount_of_random_stuff; x++)
620         place_in_arena_randomly (random_character (description ()));
621
622       for (x=0; x < ARENA_WIDTH; x++)
623         for (y=0; y < ARENA_HEIGHT; y++)
624           if (!arena[x][y])
625             place_in_arena_at_xy (gtk_label_new (NULL), x, y);
626     }
627 }
628
629 static void
630 set_up_widgets (void)
631 {
632   GtkWidget *middle = gtk_hbox_new (FALSE, 0);
633   GtkWidget *buttons = gtk_hbox_new (TRUE, 0);
634   GtkWidget *explain = NULL, *button, *intro;
635   const char *explanation =
636     "In this game, you are robot (#). "
637     "Your job is to find kitten. This task is complicated "
638     "by the existence of various things which are not kitten. "
639     "Robot must touch items to determine if they are kitten or "
640     "not. The game ends when robotfindskitten. You may move "
641     "robot about by tapping on any side of robot, or with the "
642     "arrow keys.";
643   GKeyFile *desktop = g_key_file_new ();
644   gchar *version;
645   guint x, y;
646   HildonAppMenu *menu = HILDON_APP_MENU (hildon_app_menu_new ());
647   
648   /* The window */
649
650   window = hildon_window_new ();
651   gtk_window_set_title (GTK_WINDOW (window), "robotfindskitten");
652   gtk_widget_modify_bg (window, GTK_STATE_NORMAL, &black);
653   g_signal_connect (G_OBJECT (window), "button-press-event", G_CALLBACK (on_window_clicked), NULL);
654   g_signal_connect (G_OBJECT (window), "key-press-event", G_CALLBACK (on_key_pressed), NULL);
655   g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (gtk_main_quit), NULL);
656
657   /* The prologue */
658
659   /* Get the rather odd version string.  The RFK spec says that
660    * it should read v<major>.<minor>.<number-of-NKIs>.
661    */
662   if (g_key_file_load_from_file (desktop,
663                                  "/usr/share/applications/hildon/rfk.desktop",
664                                  G_KEY_FILE_NONE,
665                                  NULL))
666     {
667       version = g_strdup_printf("v%s.%d",
668                                 g_key_file_get_value (desktop, "Desktop Entry", "Version", NULL),
669                                 nki_count);
670       g_key_file_free (desktop);
671     }
672   else
673     {
674       version = g_strdup("");
675     }
676
677   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_THUMB_HEIGHT,
678                                              HILDON_BUTTON_ARRANGEMENT_HORIZONTAL,
679                                              "Play", NULL);
680   g_signal_connect (button, "clicked", G_CALLBACK (play_game), NULL);
681
682   gtk_box_pack_end (GTK_BOX (buttons), button, TRUE, TRUE, 0);
683
684   button = hildon_button_new_with_text (HILDON_SIZE_AUTO_WIDTH | HILDON_SIZE_THUMB_HEIGHT,
685                                              HILDON_BUTTON_ARRANGEMENT_HORIZONTAL,
686                                              "Help", NULL);
687   g_signal_connect (button, "clicked", G_CALLBACK (get_help), NULL);
688   gtk_box_pack_end (GTK_BOX (buttons), button, TRUE, TRUE, 0);
689
690   /* and another help button, this time for the menu */
691   button = gtk_button_new_with_label ("Help");
692   g_signal_connect (button, "clicked", G_CALLBACK (get_help), NULL);
693   hildon_app_menu_append (menu, GTK_BUTTON (button));
694
695   button = gtk_button_new_with_label ("Restart");
696   g_signal_connect (button, "clicked", G_CALLBACK (restart), NULL);
697   hildon_app_menu_append (menu, GTK_BUTTON (button));
698
699   gtk_widget_show_all (GTK_WIDGET (menu));
700   hildon_window_set_app_menu (HILDON_WINDOW (window), menu);
701
702   explain = gtk_label_new (explanation);
703   gtk_label_set_line_wrap (GTK_LABEL (explain), TRUE);
704
705   gtk_box_pack_end (GTK_BOX (middle), explain, TRUE, TRUE, 0);
706   gtk_box_pack_end (GTK_BOX (middle), gtk_image_new_from_pixbuf (robot_pic), FALSE, FALSE, 0);
707
708   intro = gtk_vbox_new (FALSE, 0);
709   gtk_box_pack_end (GTK_BOX (intro), buttons, FALSE, FALSE, 0);
710   gtk_box_pack_end (GTK_BOX (intro), middle, TRUE, TRUE, 0);
711   gtk_box_pack_end (GTK_BOX (intro), gtk_label_new (version), FALSE, FALSE, 0);
712   g_free (version);
713
714   state_widget[STATE_PROLOGUE] = intro;
715
716   /* The game itself */
717
718   state_widget[STATE_PLAYING] = gtk_table_new (ARENA_HEIGHT, ARENA_WIDTH, TRUE);
719   g_signal_connect (state_widget[STATE_PLAYING], "parent-set", G_CALLBACK (set_up_board), NULL);
720
721   for (x=0; x < ARENA_WIDTH; x++)
722     for (y=0; y < ARENA_HEIGHT; y++)
723       arena[x][y] = NULL;
724
725   /* The epilogue */
726   state_widget[STATE_EPILOGUE] =  gtk_drawing_area_new ();
727   g_signal_connect (state_widget[STATE_EPILOGUE], "parent-set", G_CALLBACK (ending_animation), NULL);
728   g_signal_connect (G_OBJECT (state_widget[STATE_EPILOGUE]),
729                     "expose_event", G_CALLBACK (ending_animation_draw), NULL);
730
731   for (x=0; x<STATE_LAST; x++)
732     {
733       /* so we don't lose them when we take them offscreen */
734       g_object_ref (state_widget[x]);
735     }
736 }
737
738 /****************************************************************/
739 /* Let's kick the whole thing off...                            */
740 /****************************************************************/
741
742 int
743 main (gint argc,
744       gchar **argv)
745 {
746   gtk_init (&argc, &argv);
747   g_set_application_name ("robotfindskitten");
748   srandom (time(0));
749
750   ensure_messages_loaded ();
751   load_images ();
752
753   set_up_widgets ();
754   switch_state (STATE_PROLOGUE);
755   
756   gtk_main ();
757
758   return EXIT_SUCCESS;
759 }