Add to git.
[monolith.git] / examples / 04_animal_vegetable_mineral.c
1 /* Animal, vegetable, mineral; or "20 questions"
2  * - by Richard W.M. Jones <rich@annexia.org>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Library General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library 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 GNU
12  * Library General Public License for more details.
13  *
14  * You should have received a copy of the GNU Library General Public
15  * License along with this library; if not, write to the Free
16  * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17  *
18  * $Id: 04_animal_vegetable_mineral.c,v 1.8 2003/02/08 15:53:32 rich Exp $
19  */
20
21 #include <stdio.h>
22 #include <string.h>
23 #include <ctype.h>
24 #include <assert.h>
25
26 #include <pool.h>
27 #include <pstring.h>
28 #include <pthr_cgi.h>
29
30 #include "monolith.h"
31 #include "ml_window.h"
32 #include "ml_dialog.h"
33 #include "ml_form.h"
34 #include "ml_form_text.h"
35 #include "ml_form_textarea.h"
36 #include "ml_form_submit.h"
37 #include "ml_table_layout.h"
38 #include "ml_text_label.h"
39
40 /* Main entry point to the app. */
41 static void app_main (ml_session);
42
43 int
44 handle_request (rws_request rq)
45 {
46   return ml_entry_point (rq, app_main);
47 }
48
49 /* This is the location of the questions database. You might want to
50  * put this somewhere more permanent than just in /tmp.
51  */
52 #define DATABASE_FILE "/tmp/questions_database"
53
54 /* Private per-session data. */
55 struct data
56 {
57   ml_window win;                /* Top-level window. */
58   ml_dialog dialog;             /* Dialog widget. */
59   ml_form_text input1;          /* Input fields on the final form. */
60   ml_form_textarea input2;
61   ml_form_submit input3, input4;
62   int current_node;             /* The node we are currently displaying. */
63 };
64
65 static void start_game (ml_session, void *);
66
67 static void
68 app_main (ml_session session)
69 {
70   pool pool = ml_session_pool (session);
71   struct data *data;
72
73   /* Create the per-session data area. */
74   data = pmalloc (pool, sizeof *data);
75
76   /* Create the top-level window. */
77   data->win = new_ml_window (session, pool);
78
79   /* Create the dialog widget. */
80   data->dialog = new_ml_dialog (pool);
81   ml_dialog_set_title (data->dialog, "Animal, Vegetable, Mineral game "
82                                      "(a.k.a. \"20 questions\")");
83
84   /* Start the game. This populates the initial dialog with the first
85    * question.
86    */
87   start_game (session, data);
88
89   /* Pack everything up. */
90   ml_window_pack (data->win, data->dialog);
91 }
92
93 /* This data is shared between sessions. */
94 static pool avm_pool;           /* Pool for global allocations. */
95 static vector nodes;            /* List of nodes. */
96 static int a, v, m;             /* Index of animal, vegetable and mineral
97                                  * nodes in the list of nodes. */
98
99 static void avm_init (void) __attribute__((constructor));
100 static void avm_stop (void) __attribute__((destructor));
101
102 static void pressed_animal (ml_session, void *);
103 static void pressed_vegetable (ml_session, void *);
104 static void pressed_mineral (ml_session, void *);
105 static void pressed_yes (ml_session session, void *);
106 static void pressed_no (ml_session session, void *);
107 static void update_dialog (ml_session session, struct data *);
108 static void finished_game (ml_session session, void *);
109 static void unknown_thing (ml_session session, void *);
110 static void add_unknown_thing (ml_session session, void *);
111 static void save_database (void);
112 static int is_vowel (const char *);
113 static void remove_newlines (char *);
114
115 struct node
116 {
117   int is_question;              /* Either a question or an answer node. */
118
119   /* For question nodes, this is the text of the question. For answer
120    * nodes, this is the expected answer word.
121    */
122   const char *text;
123
124   /* For question nodes only, this is the index of the nodes to visit
125    * if the user says 'yes' or 'no' respectively.
126    */
127   int yes, no;
128 };
129
130 static void
131 avm_init ()
132 {
133   FILE *fp;
134   struct node node;
135   char buffer[512];
136
137   avm_pool = new_subpool (global_pool);
138   nodes = new_vector (avm_pool, struct node);
139
140   fp = fopen (DATABASE_FILE, "r");
141   if (fp == 0)
142     {
143       /* No initial database. Don't worry, just create one internally. */
144       a = 1;
145       v = 2;
146       m = 3;
147
148       /* Node 0 is a dummy node. */
149       vector_push_back (nodes, node);
150
151       /* Node 1 is the animal node. */
152       node.is_question = 0;
153       node.text = "horse";
154       vector_push_back (nodes, node);
155
156       /* Node 2 is the vegetable node. */
157       node.is_question = 0;
158       node.text = "leek";
159       vector_push_back (nodes, node);
160
161       /* Node 3 is the mineral node. */
162       node.is_question = 0;
163       node.text = "pumice stone";
164       vector_push_back (nodes, node);
165
166       return;
167     }
168
169   while (fgets (buffer, sizeof buffer, fp))
170     {
171       pchomp (buffer);
172
173       if (vector_size (nodes) == 0) /* Node 0 is special. */
174         {
175           if (sscanf (buffer, "q %d %d %d", &a, &v, &m)
176               != 3)
177             {
178               fprintf (stderr, "%s: database is corrupt\n", DATABASE_FILE);
179               abort ();
180             }
181           vector_push_back (nodes, node);
182
183 #if 0
184           fprintf (stderr, "a = %d, v = %d, m = %d\n",
185                    a, v, m);
186 #endif
187         }
188       else
189         {
190           if (buffer[0] == 'q') /* Question node. */
191             {
192               int n;
193
194               node.is_question = 1;
195
196               if (sscanf (buffer, "q %d %d %n", &node.yes, &node.no, &n) < 2)
197                 {
198                   fprintf (stderr, "%s: database is corrupt\n", DATABASE_FILE);
199                   abort ();
200                 }
201
202               node.text = pstrdup (avm_pool, &buffer[n]);
203
204               vector_push_back (nodes, node);
205
206 #if 0
207               fprintf (stderr, "yes = %d, no = %d, text = %s\n",
208                        node.yes, node.no, node.text);
209 #endif
210             }
211           else                  /* Answer node. */
212             {
213               node.is_question = 0;
214
215               node.text = pstrdup (avm_pool, &buffer[2]);
216
217               vector_push_back (nodes, node);
218
219 #if 0
220               fprintf (stderr, "text = %s\n",
221                        node.text);
222 #endif
223             }
224         }
225     }
226
227 #if 0
228   fprintf (stderr, "finished\n");
229 #endif
230
231   fclose (fp);
232 }
233
234 static void
235 avm_stop ()
236 {
237   delete_pool (avm_pool);
238 }
239
240 static void
241 save_database ()
242 {
243   FILE *fp;
244   int i;
245   struct node node;
246
247   fp = fopen (DATABASE_FILE, "w");
248   if (fp == 0)
249     {
250       perror (DATABASE_FILE);
251       return;
252     }
253
254   /* Node 0 is special. */
255   fprintf (fp, "q %d %d %d\n", a, v, m);
256
257   /* Write out the other nodes in order. */
258   for (i = 1; i < vector_size (nodes); ++i)
259     {
260       vector_get (nodes, i, node);
261
262       if (node.is_question)
263         fprintf (fp, "q %d %d %s\n", node.yes, node.no, node.text);
264       else
265         fprintf (fp, "a %s\n", node.text);
266     }
267
268   fclose (fp);
269 }
270
271 static void
272 start_game (ml_session session, void *vp)
273 {
274   struct data *data = (struct data *) vp;
275
276   data->current_node = 0;
277
278   /* Ask the initial standard question. */
279   ml_dialog_set_text (data->dialog,
280                       "<p>Think of something, anything.</p>"
281                       "<p>I will ask you questions about it "
282                       "and try to guess what it is.</p>"
283                       "<p>First question: Is it an animal, a vegetable "
284                       "or a mineral?</p>");
285   ml_dialog_clear_buttons (data->dialog);
286   ml_dialog_add_button (data->dialog, "Animal",
287                         pressed_animal, session, data);
288   ml_dialog_add_button (data->dialog, "Vegetable",
289                         pressed_vegetable, session, data);
290   ml_dialog_add_button (data->dialog, "Mineral",
291                         pressed_mineral, session, data);
292 }
293
294 static void
295 pressed_animal (ml_session session, void *vp)
296 {
297   struct data *data = (struct data *) vp;
298
299   data->current_node = a;
300   update_dialog (session, data);
301 }
302
303 static void
304 pressed_vegetable (ml_session session, void *vp)
305 {
306   struct data *data = (struct data *) vp;
307
308   data->current_node = v;
309   update_dialog (session, data);
310 }
311
312 static void
313 pressed_mineral (ml_session session, void *vp)
314 {
315   struct data *data = (struct data *) vp;
316
317   data->current_node = m;
318   update_dialog (session, data);
319 }
320
321 static void
322 pressed_yes (ml_session session, void *vp)
323 {
324   struct data *data = (struct data *) vp;
325   struct node node;
326
327   vector_get (nodes, data->current_node, node);
328   data->current_node = node.yes;
329   update_dialog (session, data);
330 }
331
332 static void
333 pressed_no (ml_session session, void *vp)
334 {
335   struct data *data = (struct data *) vp;
336   struct node node;
337
338   vector_get (nodes, data->current_node, node);
339   data->current_node = node.no;
340   update_dialog (session, data);
341 }
342
343 static void
344 update_dialog (ml_session session, struct data *data)
345 {
346   pool pool = ml_session_pool (session);
347   struct node node;
348
349   /* Fetch the current node. */
350   vector_get (nodes, data->current_node, node);
351
352   if (node.is_question)         /* Question node. */
353     {
354       /* Present the dialog window. */
355       ml_dialog_set_text (data->dialog, node.text);
356
357       /* Update the buttons. */
358       ml_dialog_clear_buttons (data->dialog);
359       ml_dialog_add_button (data->dialog, "Yes",
360                             pressed_yes, session, data);
361       ml_dialog_add_button (data->dialog, "No",
362                             pressed_no, session, data);
363     }
364   else                          /* Answer node. */
365     {
366       /* Present the dialog window. */
367       ml_dialog_set_text (data->dialog,
368                           psprintf (pool,
369                                     "Is it %s %s?",
370                                     (!is_vowel (node.text) ? "a" : "an"),
371                                     node.text));
372
373       /* Update the buttons. */
374       ml_dialog_clear_buttons (data->dialog);
375       ml_dialog_add_button (data->dialog, "Yes",
376                             finished_game, session, data);
377       ml_dialog_add_button (data->dialog, "No",
378                             unknown_thing, session, data);
379     }
380 }
381
382 static int
383 is_vowel (const char *text)
384 {
385   while (*text)
386     {
387       if (isalpha ((int) *text))
388         {
389           return *text == 'a' || *text == 'e' || *text == 'i' ||
390             *text == 'o' || *text == 'u' || *text == 'A' || *text == 'E' ||
391             *text == 'I' || *text == 'O' || *text == 'U';
392         }
393
394       text++;
395     }
396   return 0;
397 }
398
399 static void
400 finished_game (ml_session session, void *vp)
401 {
402   struct data *data = (struct data *) vp;
403
404   ml_dialog_set_text (data->dialog, "Thanks for playing!");
405   ml_dialog_clear_buttons (data->dialog);
406   ml_dialog_add_button (data->dialog, "Play again",
407                         start_game, session, data);
408 }
409
410 static void
411 unknown_thing (ml_session session, void *vp)
412 {
413   pool pool = ml_session_pool (session);
414   struct data *data = (struct data *) vp;
415   ml_form form;
416   ml_table_layout tbl;
417   ml_text_label text1, text2, text3;
418   struct node node;
419
420   vector_get (nodes, data->current_node, node);  /* Fetch the node. */
421
422   /* Generate the form used to ask the user to input a new question. */
423   form = new_ml_form (pool);
424   ml_form_set_callback (form, add_unknown_thing, session, data);
425
426   /* The form contains a table layout ... */
427   tbl = new_ml_table_layout (pool, 6, 2);
428
429   /* Populate the form. */
430   text1 = new_ml_text_label (pool,
431                              "I give up! What was the thing you were "
432                              "thinking of?");
433   ml_table_layout_pack (tbl, text1, 0, 0);
434   ml_table_layout_set_colspan (tbl, 0, 0, 2);
435
436   data->input1 = new_ml_form_text (pool, form);
437   ml_table_layout_pack (tbl, data->input1, 1, 0);
438   ml_table_layout_set_colspan (tbl, 1, 0, 2);
439
440   text2 = new_ml_text_label
441     (pool,
442      psprintf (pool,
443                "Give me a simple yes/no question which I can "
444                "ask people, to distinguish between your thing "
445                "and %s %s.",
446                (!is_vowel (node.text) ? "a" : "an"),
447                node.text));
448   ml_table_layout_pack (tbl, text2, 2, 0);
449   ml_table_layout_set_colspan (tbl, 2, 0, 2);
450
451   data->input2 = new_ml_form_textarea (pool, form, 4, 60);
452   ml_table_layout_pack (tbl, data->input2, 3, 0);
453   ml_table_layout_set_colspan (tbl, 3, 0, 2);
454
455   text3 = new_ml_text_label
456     (pool,
457      "And if I asked the question above of someone, and they "
458      "were thinking of your thing, would they answer Yes or No?");
459   ml_table_layout_pack (tbl, text3, 4, 0);
460   ml_table_layout_set_colspan (tbl, 4, 0, 2);
461
462   data->input3 = new_ml_form_submit (pool, form, "Yes");
463   ml_table_layout_pack (tbl, data->input3, 5, 0);
464
465   data->input4 = new_ml_form_submit (pool, form, "No");
466   ml_table_layout_pack (tbl, data->input4, 5, 1);
467
468   /* Pack. */
469   ml_form_pack (form, tbl);
470   ml_window_pack (data->win, form);
471 }
472
473 static void
474 add_unknown_thing (ml_session session, void *vp)
475 {
476   pool pool = ml_session_pool (session);
477   struct data *data = (struct data *) vp;
478   int old_ans, new_ans;
479   struct node old_ans_node, new_q_node, new_ans_node;
480   char *name, *question;
481   int yes_button, no_button;
482
483   vector_get (nodes, data->current_node, old_ans_node); /* Fetch the node. */
484
485   /* Extract the values of the input fields. */
486   name = pstrdup (pool, ml_form_input_get_value (data->input1));
487   question = pstrdup (pool, ml_form_input_get_value (data->input2));
488   yes_button = ml_form_input_get_value (data->input3) ? 1 : 0;
489   no_button = ml_form_input_get_value (data->input4) ? 1 : 0;
490
491   assert ((yes_button || no_button) && ! (yes_button && no_button));
492
493   /* Remove newlines from the fields (replace with whitespace). This is
494    * because the database file cannot handle newlines.
495    */
496   remove_newlines (name);
497   remove_newlines (question);
498
499   /* Update the tree. */
500   old_ans = vector_size (nodes);
501   vector_push_back (nodes, old_ans_node);
502
503   new_ans = vector_size (nodes);
504   assert (new_ans == old_ans + 1);
505   new_ans_node.is_question = 0;
506   new_ans_node.text = pstrdup (avm_pool, name);
507   vector_push_back (nodes, new_ans_node);
508
509   new_q_node.is_question = 1;
510   new_q_node.text = pstrdup (avm_pool, question);
511   new_q_node.yes = yes_button ? new_ans : old_ans;
512   new_q_node.no = no_button ? new_ans : old_ans;
513   vector_replace (nodes, data->current_node, new_q_node);
514
515   save_database ();
516
517   /* Replace the form with the original dialog. */
518   ml_window_pack (data->win, data->dialog);
519
520   /* Restart the game. */
521   start_game (session, data);
522 }
523
524 static void
525 remove_newlines (char *str)
526 {
527   while (*str)
528     {
529       if (*str == '\n') *str = ' ';
530       str++;
531     }
532 }