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