]> git.cworth.org Git - dvonn/blob - dvonn.c
Remove useless linking against loudmouth
[dvonn] / dvonn.c
1 /*
2  * Copyright (C) 2009 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 3 of the License, or
7  * (at your option) 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, see http://www.gnu.org/licenses/ .
16  *
17  * Author: Carl Worth <cworth@cworth.org>
18  */
19
20 #define _GNU_SOURCE /* for vasprintf */
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <stdarg.h>
24 #include <gtk/gtk.h>
25 #include <math.h>
26
27 #include "dvonn-board.h"
28
29 #define BOARD_X_SIZE 11
30 #define BOARD_Y_SIZE 5
31
32 typedef struct {
33     int x_offset;
34     int y_offset;
35     int width;
36     int height;
37     int cell_size;
38 } layout_t;
39
40 typedef struct _dvonn_game dvonn_game_t;
41
42 typedef struct {
43     dvonn_game_t *game;
44     GtkWidget *window;
45     layout_t layout;
46 } view_t;
47
48 struct _dvonn_game {
49     view_t **views;
50     int num_views;
51
52     dvonn_board_t board;
53     dvonn_bool_t has_selected;
54     int selected_x;
55     int selected_y;
56
57     PangoFontDescription *font;
58 };
59
60 static gboolean
61 on_delete_event_quit (GtkWidget  *widget,
62                       GdkEvent   *event,
63                       gpointer    user_data)
64 {
65     gtk_main_quit ();
66
67     /* Returning FALSE allows the default handler for delete-event
68      * to proceed to cleanup the widget. */
69     return FALSE;
70 }
71
72 /* Something like buff */
73 #define BACKGROUND_COLOR 0.89, 0.70, 0.40
74
75 #define RED_RING_COLOR 0.8, 0.2, 0.2
76
77 /* Relative to a unit square. */
78 #define RING_OUTER_RADIUS 0.4
79 #define RING_INNER_RADIUS 0.2
80 #define FONT_SIZE (RING_INNER_RADIUS * 1.5)
81
82 /* XXX: This really should have an interest rectangle. */
83 static void
84 dvonn_game_update_windows (dvonn_game_t *game)
85 {
86     int i;
87
88     for (i = 0; i < game->num_views; i++)
89         gtk_widget_queue_draw (game->views[i]->window);
90 }
91
92 /* Convert from a board index to a device-pixel coordinate pair, (at
93  * the center of the ring). */
94 static void
95 layout_board_to_device (layout_t *layout, double *x_ret, double *y_ret)
96 {
97     double x = *x_ret, y = *y_ret;
98
99     *x_ret = layout->x_offset + layout->cell_size *
100         (x + (y - DVONN_BOARD_Y_SIZE/2)/2.0 + 0.5);
101     *y_ret = layout->y_offset + layout->cell_size * (M_SQRT1_2 * y + 0.5);
102 }
103
104 /* Convert from a device-pixel coordinate pair to a board index. */
105 static void
106 layout_device_to_board (layout_t *layout, int *x_ret, int *y_ret)
107 {
108     int x = *x_ret, y = *y_ret;
109     int x1, y1, x2, y2;
110     double x1_dev, y1_dev, x2_dev, y2_dev;
111     double dx, dy, d1, d2;
112
113     /* Because the vertical spacing between adjacent rows is less than
114      * layout->cell_size, the simple calculations here give us two
115      * candidate (x,y) pairs. We then choose the correct one based on
116      * a distance calculation.
117      */
118     y1 = (y - layout->y_offset) / (layout->cell_size * M_SQRT1_2);
119     x1 = (double) (x - layout->x_offset) / layout->cell_size - (y1 - DVONN_BOARD_Y_SIZE/2)/2.0;
120
121     y2 = y1 - 1;
122     x2 = (double) (x - layout->x_offset) / layout->cell_size - (y2 - DVONN_BOARD_Y_SIZE/2)/2.0;
123
124     x1_dev = x1;
125     y1_dev = y1;
126     layout_board_to_device (layout, &x1_dev, &y1_dev);
127
128     x2_dev = x2;
129     y2_dev = y2;
130     layout_board_to_device (layout, &x2_dev, &y2_dev);
131
132     dx = x - x1_dev;
133     dy = y - y1_dev;
134     d1 = sqrt (dx*dx + dy*dy);
135
136     dx = x - x2_dev;
137     dy = y - y2_dev;
138     d2 = sqrt (dx*dx + dy*dy);
139
140     if (d1 < d2) {
141         *x_ret = x1;
142         *y_ret = y1;
143     } else {
144         *x_ret = x2;
145         *y_ret = y2;
146     }
147 }
148
149 static gboolean
150 on_button_press_event (GtkWidget        *widget,
151                        GdkEventButton   *event,
152                        gpointer          user_data)
153 {
154     view_t *view = user_data;
155     layout_t *layout = &view->layout;
156     dvonn_game_t *game = view->game;
157     int x, y;
158     char *error;
159
160     /* Ignore double and triple clicks. */
161     if (event->type >= GDK_2BUTTON_PRESS)
162         return TRUE;
163
164     /* Right-click means pass, (yes, it would be better to add some
165      * actual UI elements... */
166     if (event->button == 3) {
167         dvonn_board_pass (&game->board);
168         return TRUE;
169     }
170
171     x = event->x;
172     y = event->y;
173     layout_device_to_board (layout, &x, &y);
174
175     /* Do nothing for out-of-bounds clicks */
176     if (x < 0 || x >= DVONN_BOARD_X_SIZE ||
177         y < 0 || y >= DVONN_BOARD_Y_SIZE)
178     {
179         return TRUE;
180     }
181
182     /* Nor for cells which have array entries that are invalid. */
183     if (game->board.cells[x][y].type == DVONN_CELL_INVALID)
184         return TRUE;
185
186     if (game->board.phase == DVONN_PHASE_PLACEMENT) {
187         if (dvonn_board_place (&game->board, x, y, &error))
188             dvonn_game_update_windows (game);
189         else
190             printf ("Illegal placement %c%d: %s\n",
191                     'a' + x,
192                     y + 1,
193                     error);
194
195         return TRUE;
196     }
197
198     if (! game->has_selected) {
199         if (game->board.cells[x][y].type == game->board.player &&
200             ! dvonn_board_cell_surrounded (&game->board, x, y))
201             {
202                     game->has_selected = TRUE;
203                     game->selected_x = x;
204                     game->selected_y = y;
205                     dvonn_game_update_windows (game);
206             }
207         return TRUE;
208     }
209
210     if (x == game->selected_x && y == game->selected_y)
211     {
212         game->has_selected = FALSE;
213         dvonn_game_update_windows (game);
214         return TRUE;
215     }
216         
217     if (dvonn_board_move (&game->board,
218                           game->selected_x, game->selected_y,
219                           x, y, &error))
220     {
221         game->has_selected = FALSE;
222         dvonn_game_update_windows (game);
223         return TRUE;
224     } else {
225         printf ("Illegal move %c%d%c%d: %s\n",
226                 'a' + game->selected_x,
227                 game->selected_y + 1,
228                 'a' + x,
229                 y + 1,
230                 error);
231     }
232
233     return TRUE;
234 }
235
236 /* Add a unit-sized DVONN-ring path to cr, from (0,0) to (1,1). */
237 static void
238 ring_path (cairo_t *cr)
239 {
240     cairo_new_sub_path (cr);
241     cairo_arc (cr, 0.5, 0.5, RING_OUTER_RADIUS, 0, 2 * M_PI);
242     cairo_arc_negative (cr, 0.5, 0.5, RING_INNER_RADIUS, 2 * M_PI, 0);
243 }
244
245 /* Some helper functions for using pango. */
246 static PangoLayout *
247 _create_layout (cairo_t *cr, PangoFontDescription *font, const char *text)
248 {
249     PangoLayout *layout;
250
251     layout = pango_cairo_create_layout (cr);
252     pango_layout_set_font_description (layout, font);
253     pango_layout_set_text (layout, text, -1);
254     pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER);
255
256     return layout;
257 }
258
259 #define PRINTF_FORMAT(fmt_index, va_index) __attribute__ ((__format__(__printf__, fmt_index, va_index)))
260
261 static PangoLayout *
262 _create_layout_vprintf (cairo_t *cr, PangoFontDescription *font, const char *fmt, va_list ap)
263 {
264     PangoLayout *layout;
265     char *text;
266
267     vasprintf (&text, fmt, ap);
268
269     layout = _create_layout (cr, font, text);
270
271     free (text);
272
273     return layout;
274 }
275
276 static PangoLayout *
277 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
278     PRINTF_FORMAT (3, 4);
279
280 static PangoLayout *
281 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
282 {
283     va_list ap;
284     PangoLayout *layout;
285
286     va_start (ap, fmt);
287
288     layout = _create_layout_vprintf (cr, font, fmt, ap);
289
290     va_end (ap);
291
292     return layout;
293 }
294
295 static void
296 _destroy_layout (PangoLayout *layout)
297 {
298     g_object_unref (layout);
299 }
300
301 static void
302 _show_layout (cairo_t *cr, PangoLayout *layout)
303 {
304     pango_cairo_show_layout (cr, layout);
305
306     _destroy_layout (layout);
307 }
308
309 static gboolean
310 on_expose_event_draw (GtkWidget         *widget,
311                       GdkEventExpose    *event,
312                       gpointer           user_data)
313 {
314     view_t *view = user_data;
315     layout_t *layout = &view->layout;
316     dvonn_game_t *game = view->game;
317     cairo_t *cr;
318     int x, y;
319
320     if (layout->width != widget->allocation.width ||
321         layout->height != widget->allocation.height)
322     {
323         int x_size, y_size;
324
325         layout->width = widget->allocation.width;
326         layout->height = widget->allocation.height;
327
328         x_size = layout->width;
329         if (x_size > layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1)))
330             x_size = layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
331
332         /* Size must be a multiple of the integer cell_size */
333         layout->cell_size = x_size / BOARD_X_SIZE;
334         x_size = layout->cell_size * BOARD_X_SIZE;
335         y_size = layout->cell_size * (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
336
337         layout->x_offset = (layout->width - x_size) / 2;
338         layout->y_offset = (layout->height - y_size) / 2;
339
340         if (game->font == NULL) {
341             game->font = pango_font_description_new ();
342             pango_font_description_set_family (game->font, "sans");
343         }
344         pango_font_description_set_absolute_size (game->font,
345                                                   FONT_SIZE * PANGO_SCALE);
346     }
347
348     cr = gdk_cairo_create (widget->window);
349
350     cairo_set_source_rgb (cr, BACKGROUND_COLOR);
351     cairo_paint (cr);
352
353     cairo_translate (cr, layout->x_offset, layout->y_offset);
354     cairo_scale (cr, layout->cell_size, layout->cell_size);
355
356     for (y = 0; y < BOARD_Y_SIZE; y++) {
357         for (x = 0; x < BOARD_X_SIZE; x++) {
358             dvonn_cell_t cell;
359
360             cell = game->board.cells[x][y];
361             if (cell.type == DVONN_CELL_INVALID)
362                 continue;
363
364             cairo_save (cr);
365             cairo_translate(cr,
366                             x + (y - DVONN_BOARD_Y_SIZE/2) / 2.0,
367                             M_SQRT1_2 * y);
368             if (cell.contains_red && cell.type != DVONN_CELL_RED) {
369                 cairo_new_sub_path (cr);
370                 cairo_arc (cr, 0.5, 0.5,
371                            (RING_INNER_RADIUS + RING_OUTER_RADIUS)/2.0,
372                            0, 2 * M_PI);
373                 cairo_set_source_rgb (cr, RED_RING_COLOR);
374                 cairo_fill (cr);
375             }
376             ring_path (cr);
377             switch (cell.type) {
378             case DVONN_CELL_WHITE:
379                 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
380                 break;
381             case DVONN_CELL_BLACK:
382                 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
383                 break;
384             case DVONN_CELL_RED:
385                 cairo_set_source_rgb (cr, RED_RING_COLOR);
386                 break;
387             case DVONN_CELL_EMPTY:
388             default:
389                 cairo_set_source_rgba (cr, 0.0, 0.0, 0.2, 0.1);
390                 break;
391             }
392             if (game->has_selected &&
393                 x == game->selected_x &&
394                 y == game->selected_y)
395             {
396                 cairo_fill_preserve (cr);
397                 cairo_set_source_rgba (cr, 0.2, 0.2, 1.0, 0.4);
398             }
399             cairo_fill (cr);
400
401             if (game->board.cells[x][y].height > 1) {
402                 PangoLayout *height;
403                 cairo_move_to (cr,
404                                0.5 - 0.7 * RING_INNER_RADIUS * cos (M_PI_4),
405                                0.5 - 1.2 * RING_INNER_RADIUS * sin (M_PI_4));
406                 height = _create_layout_printf (cr, game->font, "%d",
407                                                 game->board.cells[x][y].height);
408                 _show_layout (cr, height);
409             }
410
411             cairo_restore (cr);
412         }
413     }
414
415     cairo_destroy (cr);
416
417     return TRUE;
418 }
419
420 static void
421 dvonn_game_init (dvonn_game_t *game)
422 {
423     game->views = NULL;
424     game->num_views = 0;
425
426     game->has_selected = FALSE;
427
428     dvonn_board_init (&game->board);
429
430     game->font = NULL;
431 }
432
433 static void
434 view_init (view_t *view, dvonn_game_t *game, GtkWidget *window)
435 {
436     view->game = game;
437     view->window = window;
438
439     view->layout.width = 0;
440     view->layout.height = 0;
441 }
442
443 static void
444 dvonn_game_create_view (dvonn_game_t *game)
445 {
446     view_t *view;
447     GtkWidget *window;
448     GtkWidget *drawing_area;
449
450     window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
451
452     view = malloc (sizeof (view_t));
453     if (view == NULL) {
454         fprintf (stderr, "Out of memory.\n");
455         exit (1);
456     }
457
458     game->num_views++;
459     game->views = realloc (game->views,
460                            game->num_views * sizeof (view_t *));
461     if (game->views == NULL) {
462         fprintf (stderr, "Out of memory.\n");
463         exit (1);
464     }
465     game->views[game->num_views - 1] = view;
466
467     view_init (view, game, window);
468
469     gtk_window_set_default_size (GTK_WINDOW (window), 780, 251);
470
471     g_signal_connect (window, "delete-event",
472                       G_CALLBACK (on_delete_event_quit), NULL);
473
474     drawing_area = gtk_drawing_area_new ();
475
476     gtk_container_add (GTK_CONTAINER (window), drawing_area);
477
478     g_signal_connect (drawing_area, "expose-event",  
479                       G_CALLBACK (on_expose_event_draw), view);
480
481     gtk_widget_add_events (drawing_area, GDK_BUTTON_PRESS_MASK);
482     g_signal_connect (drawing_area, "button-press-event",
483                       G_CALLBACK (on_button_press_event), view);
484
485     gtk_widget_show_all (window);
486 }
487
488 int
489 main (int argc, char *argv[])
490 {
491     dvonn_game_t game;
492
493     dvonn_game_init (&game);
494
495     gtk_init (&argc, &argv);
496
497     /* Create two views of the game (one for each player) */
498     dvonn_game_create_view (&game);
499     dvonn_game_create_view (&game);
500     
501     gtk_main ();
502
503     return 0;
504 }