]> git.cworth.org Git - dvonn/blob - dvonn.c
Add visual indication for non-red stacks containing a red piece
[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     x = event->x;
161     y = event->y;
162     layout_device_to_board (layout, &x, &y);
163
164     /* Do nothing for out-of-bounds clicks */
165     if (x < 0 || x >= DVONN_BOARD_X_SIZE ||
166         y < 0 || y >= DVONN_BOARD_Y_SIZE)
167     {
168         return TRUE;
169     }
170
171     /* Nor for cells which have array entries that are invalid. */
172     if (game->board.cells[x][y].type == DVONN_CELL_INVALID)
173         return TRUE;
174
175     if (game->board.phase == DVONN_PHASE_PLACEMENT) {
176         if (dvonn_board_place (&game->board, x, y, &error))
177             dvonn_game_update_windows (game);
178         else
179             printf ("Illegal placement %c%d: %s\n",
180                     'a' + x,
181                     y + 1,
182                     error);
183
184         return TRUE;
185     }
186
187     if (! game->has_selected) {
188         if (game->board.cells[x][y].type == game->board.player) {
189             game->has_selected = TRUE;
190             game->selected_x = x;
191             game->selected_y = y;
192             dvonn_game_update_windows (game);
193         }
194         return TRUE;
195     }
196
197     if (x == game->selected_x && y == game->selected_y)
198     {
199         game->has_selected = FALSE;
200         dvonn_game_update_windows (game);
201         return TRUE;
202     }
203         
204     if (dvonn_board_move (&game->board,
205                           game->selected_x, game->selected_y,
206                           x, y, &error))
207     {
208         game->has_selected = FALSE;
209         dvonn_game_update_windows (game);
210         return TRUE;
211     } else {
212         printf ("Illegal move %c%d%c%d: %s\n",
213                 'a' + game->selected_x,
214                 game->selected_y + 1,
215                 'a' + x,
216                 y + 1,
217                 error);
218     }
219
220     return TRUE;
221 }
222
223 /* Add a unit-sized DVONN-ring path to cr, from (0,0) to (1,1). */
224 static void
225 ring_path (cairo_t *cr)
226 {
227     cairo_new_sub_path (cr);
228     cairo_arc (cr, 0.5, 0.5, RING_OUTER_RADIUS, 0, 2 * M_PI);
229     cairo_arc_negative (cr, 0.5, 0.5, RING_INNER_RADIUS, 2 * M_PI, 0);
230 }
231
232 /* Some helper functions for using pango. */
233 static PangoLayout *
234 _create_layout (cairo_t *cr, PangoFontDescription *font, const char *text)
235 {
236     PangoLayout *layout;
237
238     layout = pango_cairo_create_layout (cr);
239     pango_layout_set_font_description (layout, font);
240     pango_layout_set_text (layout, text, -1);
241     pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER);
242
243     return layout;
244 }
245
246 #define PRINTF_FORMAT(fmt_index, va_index) __attribute__ ((__format__(__printf__, fmt_index, va_index)))
247
248 static PangoLayout *
249 _create_layout_vprintf (cairo_t *cr, PangoFontDescription *font, const char *fmt, va_list ap)
250 {
251     PangoLayout *layout;
252     char *text;
253
254     vasprintf (&text, fmt, ap);
255
256     layout = _create_layout (cr, font, text);
257
258     free (text);
259
260     return layout;
261 }
262
263 static PangoLayout *
264 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
265     PRINTF_FORMAT (3, 4);
266
267 static PangoLayout *
268 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
269 {
270     va_list ap;
271     PangoLayout *layout;
272
273     va_start (ap, fmt);
274
275     layout = _create_layout_vprintf (cr, font, fmt, ap);
276
277     va_end (ap);
278
279     return layout;
280 }
281
282 static void
283 _destroy_layout (PangoLayout *layout)
284 {
285     g_object_unref (layout);
286 }
287
288 static void
289 _show_layout (cairo_t *cr, PangoLayout *layout)
290 {
291     pango_cairo_show_layout (cr, layout);
292
293     _destroy_layout (layout);
294 }
295
296 static gboolean
297 on_expose_event_draw (GtkWidget         *widget,
298                       GdkEventExpose    *event,
299                       gpointer           user_data)
300 {
301     view_t *view = user_data;
302     layout_t *layout = &view->layout;
303     dvonn_game_t *game = view->game;
304     cairo_t *cr;
305     int x, y;
306
307     if (layout->width != widget->allocation.width ||
308         layout->height != widget->allocation.height)
309     {
310         int x_size, y_size;
311
312         layout->width = widget->allocation.width;
313         layout->height = widget->allocation.height;
314
315         x_size = layout->width;
316         if (x_size > layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1)))
317             x_size = layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
318
319         /* Size must be a multiple of the integer cell_size */
320         layout->cell_size = x_size / BOARD_X_SIZE;
321         x_size = layout->cell_size * BOARD_X_SIZE;
322         y_size = layout->cell_size * (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
323
324         layout->x_offset = (layout->width - x_size) / 2;
325         layout->y_offset = (layout->height - y_size) / 2;
326
327         if (game->font == NULL) {
328             game->font = pango_font_description_new ();
329             pango_font_description_set_family (game->font, "sans");
330         }
331         pango_font_description_set_absolute_size (game->font,
332                                                   FONT_SIZE * PANGO_SCALE);
333     }
334
335     cr = gdk_cairo_create (widget->window);
336
337     cairo_set_source_rgb (cr, BACKGROUND_COLOR);
338     cairo_paint (cr);
339
340     cairo_translate (cr, layout->x_offset, layout->y_offset);
341     cairo_scale (cr, layout->cell_size, layout->cell_size);
342
343     for (y = 0; y < BOARD_Y_SIZE; y++) {
344         for (x = 0; x < BOARD_X_SIZE; x++) {
345             dvonn_cell_t cell;
346
347             cell = game->board.cells[x][y];
348             if (cell.type == DVONN_CELL_INVALID)
349                 continue;
350
351             cairo_save (cr);
352             cairo_translate(cr,
353                             x + (y - DVONN_BOARD_Y_SIZE/2) / 2.0,
354                             M_SQRT1_2 * y);
355             if (cell.contains_red && cell.type != DVONN_CELL_RED) {
356                 cairo_new_sub_path (cr);
357                 cairo_arc (cr, 0.5, 0.5,
358                            (RING_INNER_RADIUS + RING_OUTER_RADIUS)/2.0,
359                            0, 2 * M_PI);
360                 cairo_set_source_rgb (cr, RED_RING_COLOR);
361                 cairo_fill (cr);
362             }
363             ring_path (cr);
364             switch (cell.type) {
365             case DVONN_CELL_WHITE:
366                 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
367                 break;
368             case DVONN_CELL_BLACK:
369                 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
370                 break;
371             case DVONN_CELL_RED:
372                 cairo_set_source_rgb (cr, RED_RING_COLOR);
373                 break;
374             case DVONN_CELL_EMPTY:
375             default:
376                 cairo_set_source_rgba (cr, 0.0, 0.0, 0.2, 0.1);
377                 break;
378             }
379             if (game->has_selected &&
380                 x == game->selected_x &&
381                 y == game->selected_y)
382             {
383                 cairo_fill_preserve (cr);
384                 cairo_set_source_rgba (cr, 0.2, 0.2, 1.0, 0.4);
385             }
386             cairo_fill (cr);
387
388             if (game->board.cells[x][y].height > 1) {
389                 PangoLayout *height;
390                 cairo_move_to (cr,
391                                0.5 - 0.7 * RING_INNER_RADIUS * cos (M_PI_4),
392                                0.5 - 1.2 * RING_INNER_RADIUS * sin (M_PI_4));
393                 height = _create_layout_printf (cr, game->font, "%d",
394                                                 game->board.cells[x][y].height);
395                 _show_layout (cr, height);
396             }
397
398             cairo_restore (cr);
399         }
400     }
401
402     cairo_destroy (cr);
403
404     return TRUE;
405 }
406
407 static void
408 dvonn_game_init (dvonn_game_t *game)
409 {
410     game->views = NULL;
411     game->num_views = 0;
412
413     game->has_selected = FALSE;
414
415     dvonn_board_init (&game->board);
416
417     game->font = NULL;
418 }
419
420 static void
421 view_init (view_t *view, dvonn_game_t *game, GtkWidget *window)
422 {
423     view->game = game;
424     view->window = window;
425
426     view->layout.width = 0;
427     view->layout.height = 0;
428 }
429
430 static void
431 dvonn_game_create_view (dvonn_game_t *game)
432 {
433     view_t *view;
434     GtkWidget *window;
435     GtkWidget *drawing_area;
436
437     window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
438
439     view = malloc (sizeof (view_t));
440     if (view == NULL) {
441         fprintf (stderr, "Out of memory.\n");
442         exit (1);
443     }
444
445     game->num_views++;
446     game->views = realloc (game->views,
447                            game->num_views * sizeof (view_t *));
448     if (game->views == NULL) {
449         fprintf (stderr, "Out of memory.\n");
450         exit (1);
451     }
452     game->views[game->num_views - 1] = view;
453
454     view_init (view, game, window);
455
456     gtk_window_set_default_size (GTK_WINDOW (window), 780, 251);
457
458     g_signal_connect (window, "delete-event",
459                       G_CALLBACK (on_delete_event_quit), NULL);
460
461     drawing_area = gtk_drawing_area_new ();
462
463     gtk_container_add (GTK_CONTAINER (window), drawing_area);
464
465     g_signal_connect (drawing_area, "expose-event",  
466                       G_CALLBACK (on_expose_event_draw), view);
467
468     gtk_widget_add_events (drawing_area, GDK_BUTTON_PRESS_MASK);
469     g_signal_connect (drawing_area, "button-press-event",
470                       G_CALLBACK (on_button_press_event), view);
471
472     gtk_widget_show_all (window);
473 }
474
475 int
476 main (int argc, char *argv[])
477 {
478     dvonn_game_t game;
479
480     dvonn_game_init (&game);
481
482     gtk_init (&argc, &argv);
483
484     /* Create two views of the game (one for each player) */
485     dvonn_game_create_view (&game);
486     dvonn_game_create_view (&game);
487     
488     gtk_main ();
489
490     return 0;
491 }