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