]> git.cworth.org Git - dvonn/blob - dvonn.c
Clean up some errant trailing whitespace.
[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     PangoFontDescription *ring_font;
59
60     dvonn_bool_t dual_window_mode;
61     GtkWidget *windows[2];
62 };
63
64 static gboolean
65 on_delete_event_quit (GtkWidget  *widget,
66                       GdkEvent   *event,
67                       gpointer    user_data)
68 {
69     gtk_main_quit ();
70
71     /* Returning FALSE allows the default handler for delete-event
72      * to proceed to cleanup the widget. */
73     return FALSE;
74 }
75
76 /* Something like buff */
77 #define BACKGROUND_COLOR 0.89, 0.70, 0.40
78
79 #define RED_RING_COLOR 0.8, 0.2, 0.2
80
81 #define DVONN_FONT "sans"
82 #define DVONN_FONT_SIZE 12
83
84 /* Relative to a unit square. */
85 #define RING_OUTER_RADIUS 0.4
86 #define RING_INNER_RADIUS 0.2
87 #define RING_FONT_SIZE (RING_INNER_RADIUS * 1.5)
88
89 /* XXX: This really should have an interest rectangle. */
90 static void
91 dvonn_game_update_windows (dvonn_game_t *game)
92 {
93     int i;
94
95     for (i = 0; i < game->num_views; i++)
96         gtk_widget_queue_draw (game->views[i]->window);
97 }
98
99 /* Convert from a board index to a device-pixel coordinate pair, (at
100  * the center of the ring). */
101 static void
102 layout_board_to_device (layout_t *layout, double *x_ret, double *y_ret)
103 {
104     double x = *x_ret, y = *y_ret;
105
106     *x_ret = layout->x_offset + layout->cell_size *
107         (x + (y - DVONN_BOARD_Y_SIZE/2)/2.0 + 0.5);
108     *y_ret = layout->y_offset + layout->cell_size * (M_SQRT1_2 * y + 0.5);
109 }
110
111 /* Convert from a device-pixel coordinate pair to a board index. */
112 static void
113 layout_device_to_board (layout_t *layout, int *x_ret, int *y_ret)
114 {
115     int x = *x_ret, y = *y_ret;
116     int x1, y1, x2, y2;
117     double x1_dev, y1_dev, x2_dev, y2_dev;
118     double dx, dy, d1, d2;
119
120     /* Because the vertical spacing between adjacent rows is less than
121      * layout->cell_size, the simple calculations here give us two
122      * candidate (x,y) pairs. We then choose the correct one based on
123      * a distance calculation.
124      */
125     y1 = (y - layout->y_offset) / (layout->cell_size * M_SQRT1_2);
126     x1 = (double) (x - layout->x_offset) / layout->cell_size - (y1 - DVONN_BOARD_Y_SIZE/2)/2.0;
127
128     y2 = y1 - 1;
129     x2 = (double) (x - layout->x_offset) / layout->cell_size - (y2 - DVONN_BOARD_Y_SIZE/2)/2.0;
130
131     x1_dev = x1;
132     y1_dev = y1;
133     layout_board_to_device (layout, &x1_dev, &y1_dev);
134
135     x2_dev = x2;
136     y2_dev = y2;
137     layout_board_to_device (layout, &x2_dev, &y2_dev);
138
139     dx = x - x1_dev;
140     dy = y - y1_dev;
141     d1 = sqrt (dx*dx + dy*dy);
142
143     dx = x - x2_dev;
144     dy = y - y2_dev;
145     d2 = sqrt (dx*dx + dy*dy);
146
147     if (d1 < d2) {
148         *x_ret = x1;
149         *y_ret = y1;
150     } else {
151         *x_ret = x2;
152         *y_ret = y2;
153     }
154 }
155
156 static gboolean
157 on_button_press_event (GtkWidget        *widget,
158                        GdkEventButton   *event,
159                        gpointer          user_data)
160 {
161     view_t *view = user_data;
162     layout_t *layout = &view->layout;
163     dvonn_game_t *game = view->game;
164     int x, y;
165     char *error;
166
167     /* Ignore events from the non-player. (Obviously when we add more
168      * interaction abilities, we will want to allow those even for the
169      * non-player---things like quit, etc.). */
170     if (game->dual_window_mode &&
171         widget->parent != game->windows[game->board.player])
172     {
173         return TRUE;
174     }
175
176     /* Ignore double and triple clicks. */
177     if (event->type >= GDK_2BUTTON_PRESS)
178         return TRUE;
179
180     /* Right-click means pass, (yes, it would be better to add some
181      * actual UI elements... */
182     if (event->button == 3) {
183         dvonn_board_pass (&game->board);
184         return TRUE;
185     }
186
187     x = event->x;
188     y = event->y;
189     layout_device_to_board (layout, &x, &y);
190
191     /* Do nothing for out-of-bounds clicks */
192     if (x < 0 || x >= DVONN_BOARD_X_SIZE ||
193         y < 0 || y >= DVONN_BOARD_Y_SIZE)
194     {
195         return TRUE;
196     }
197
198     /* Nor for cells which have array entries that are invalid. */
199     if (game->board.cells[x][y].type == DVONN_CELL_INVALID)
200         return TRUE;
201
202     if (game->board.phase == DVONN_PHASE_PLACEMENT) {
203         if (dvonn_board_place (&game->board, x, y, &error))
204             dvonn_game_update_windows (game);
205         else
206             printf ("Illegal placement %c%d: %s\n",
207                     'a' + x,
208                     y + 1,
209                     error);
210
211         return TRUE;
212     }
213
214     if (! game->has_selected) {
215         if (dvonn_board_cell_owned_by (&game->board, x, y, game->board.player) &&
216             ! dvonn_board_cell_surrounded (&game->board, x, y))
217             {
218                     game->has_selected = TRUE;
219                     game->selected_x = x;
220                     game->selected_y = y;
221                     dvonn_game_update_windows (game);
222             }
223         return TRUE;
224     }
225
226     if (x == game->selected_x && y == game->selected_y)
227     {
228         game->has_selected = FALSE;
229         dvonn_game_update_windows (game);
230         return TRUE;
231     }
232
233     if (dvonn_board_move (&game->board,
234                           game->selected_x, game->selected_y,
235                           x, y, &error))
236     {
237         game->has_selected = FALSE;
238         dvonn_game_update_windows (game);
239         return TRUE;
240     } else {
241         printf ("Illegal move %c%d%c%d: %s\n",
242                 'a' + game->selected_x,
243                 game->selected_y + 1,
244                 'a' + x,
245                 y + 1,
246                 error);
247     }
248
249     return TRUE;
250 }
251
252 /* Add a unit-sized DVONN-ring path to cr, from (0,0) to (1,1). */
253 static void
254 ring_path (cairo_t *cr)
255 {
256     cairo_new_sub_path (cr);
257     cairo_arc (cr, 0.5, 0.5, RING_OUTER_RADIUS, 0, 2 * M_PI);
258     cairo_arc_negative (cr, 0.5, 0.5, RING_INNER_RADIUS, 2 * M_PI, 0);
259 }
260
261 /* Some helper functions for using pango. */
262 static PangoLayout *
263 _create_layout (cairo_t *cr, PangoFontDescription *font, const char *text)
264 {
265     PangoLayout *layout;
266
267     layout = pango_cairo_create_layout (cr);
268     pango_layout_set_font_description (layout, font);
269     pango_layout_set_text (layout, text, -1);
270     pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER);
271
272     return layout;
273 }
274
275 #define PRINTF_FORMAT(fmt_index, va_index) __attribute__ ((__format__(__printf__, fmt_index, va_index)))
276
277 static PangoLayout *
278 _create_layout_vprintf (cairo_t *cr, PangoFontDescription *font, const char *fmt, va_list ap)
279 {
280     PangoLayout *layout;
281     char *text;
282
283     vasprintf (&text, fmt, ap);
284
285     layout = _create_layout (cr, font, text);
286
287     free (text);
288
289     return layout;
290 }
291
292 static PangoLayout *
293 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
294     PRINTF_FORMAT (3, 4);
295
296 static PangoLayout *
297 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
298 {
299     va_list ap;
300     PangoLayout *layout;
301
302     va_start (ap, fmt);
303
304     layout = _create_layout_vprintf (cr, font, fmt, ap);
305
306     va_end (ap);
307
308     return layout;
309 }
310
311 static void
312 _destroy_layout (PangoLayout *layout)
313 {
314     g_object_unref (layout);
315 }
316
317 static void
318 _show_layout (cairo_t *cr, PangoLayout *layout)
319 {
320     pango_cairo_show_layout (cr, layout);
321
322     _destroy_layout (layout);
323 }
324
325 static gboolean
326 on_expose_event_draw (GtkWidget         *widget,
327                       GdkEventExpose    *event,
328                       gpointer           user_data)
329 {
330     view_t *view = user_data;
331     layout_t *layout = &view->layout;
332     dvonn_game_t *game = view->game;
333     cairo_t *cr;
334     int x, y;
335     PangoLayout *to_move;
336
337     if (layout->width != widget->allocation.width ||
338         layout->height != widget->allocation.height)
339     {
340         int x_size, y_size;
341
342         layout->width = widget->allocation.width;
343         layout->height = widget->allocation.height;
344
345         x_size = layout->width;
346         if (x_size > layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1)))
347             x_size = layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
348
349         /* Size must be a multiple of the integer cell_size */
350         layout->cell_size = x_size / BOARD_X_SIZE;
351         x_size = layout->cell_size * BOARD_X_SIZE;
352         y_size = layout->cell_size * (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
353
354         layout->x_offset = (layout->width - x_size) / 2;
355         layout->y_offset = (layout->height - y_size) / 2;
356
357         if (game->ring_font == NULL) {
358             game->ring_font = pango_font_description_new ();
359             pango_font_description_set_family (game->ring_font,
360                                                DVONN_FONT);
361         }
362         pango_font_description_set_absolute_size (game->ring_font,
363                                                   RING_FONT_SIZE * PANGO_SCALE);
364     }
365
366     cr = gdk_cairo_create (widget->window);
367
368     cairo_set_source_rgb (cr, BACKGROUND_COLOR);
369     cairo_paint (cr);
370
371     if (game->font == NULL) {
372         game->font = pango_font_description_new ();
373         pango_font_description_set_family (game->font, DVONN_FONT);
374         pango_font_description_set_absolute_size (game->font, DVONN_FONT_SIZE * PANGO_SCALE);
375     }
376     to_move = _create_layout_printf (cr, game->font,
377                                      "%s to %s.",
378                                      game->board.player == DVONN_PLAYER_WHITE ?
379                                      "White" : "Black",
380                                      game->board.phase == DVONN_PHASE_PLACEMENT ?
381                                      "place" : "move");
382     cairo_move_to (cr, 2, 2);
383     if (game->board.player == DVONN_PLAYER_WHITE)
384         cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
385     else
386         cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
387     _show_layout (cr, to_move);
388
389     cairo_translate (cr, layout->x_offset, layout->y_offset);
390     cairo_scale (cr, layout->cell_size, layout->cell_size);
391
392     for (y = 0; y < BOARD_Y_SIZE; y++) {
393         for (x = 0; x < BOARD_X_SIZE; x++) {
394             dvonn_cell_t cell;
395
396             cell = game->board.cells[x][y];
397             if (cell.type == DVONN_CELL_INVALID)
398                 continue;
399
400             cairo_save (cr);
401             cairo_translate(cr,
402                             x + (y - DVONN_BOARD_Y_SIZE/2) / 2.0,
403                             M_SQRT1_2 * y);
404             if (cell.contains_red && cell.type != DVONN_CELL_RED) {
405                 cairo_new_sub_path (cr);
406                 cairo_arc (cr, 0.5, 0.5,
407                            (RING_INNER_RADIUS + RING_OUTER_RADIUS)/2.0,
408                            0, 2 * M_PI);
409                 cairo_set_source_rgb (cr, RED_RING_COLOR);
410                 cairo_fill (cr);
411             }
412             ring_path (cr);
413             switch (cell.type) {
414             case DVONN_CELL_WHITE:
415                 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
416                 break;
417             case DVONN_CELL_BLACK:
418                 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
419                 break;
420             case DVONN_CELL_RED:
421                 cairo_set_source_rgb (cr, RED_RING_COLOR);
422                 break;
423             case DVONN_CELL_EMPTY:
424             default:
425                 cairo_set_source_rgba (cr, 0.0, 0.0, 0.2, 0.1);
426                 break;
427             }
428             if (game->has_selected &&
429                 x == game->selected_x &&
430                 y == game->selected_y)
431             {
432                 cairo_fill_preserve (cr);
433                 cairo_set_source_rgba (cr, 0.2, 0.2, 1.0, 0.4);
434             }
435             cairo_fill (cr);
436
437             if (game->board.cells[x][y].height > 1) {
438                 PangoLayout *height;
439                 cairo_move_to (cr,
440                                0.5 - 0.7 * RING_INNER_RADIUS * cos (M_PI_4),
441                                0.5 - 1.2 * RING_INNER_RADIUS * sin (M_PI_4));
442                 height = _create_layout_printf (cr, game->ring_font, "%d",
443                                                 game->board.cells[x][y].height);
444                 _show_layout (cr, height);
445             }
446
447             cairo_restore (cr);
448         }
449     }
450
451     cairo_destroy (cr);
452
453     return TRUE;
454 }
455
456 static void
457 dvonn_game_init (dvonn_game_t *game)
458 {
459     game->views = NULL;
460     game->num_views = 0;
461
462     game->has_selected = FALSE;
463
464     dvonn_board_init (&game->board);
465
466     game->font = NULL;
467     game->ring_font = NULL;
468
469     game->dual_window_mode = 0;
470     game->windows[0] = NULL;
471     game->windows[1] = NULL;
472 }
473
474 static void
475 view_init (view_t *view, dvonn_game_t *game, GtkWidget *window)
476 {
477     view->game = game;
478     view->window = window;
479
480     view->layout.width = 0;
481     view->layout.height = 0;
482 }
483
484 static GtkWidget*
485 dvonn_game_create_view (dvonn_game_t *game)
486 {
487     view_t *view;
488     GtkWidget *window;
489     GtkWidget *drawing_area;
490
491     window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
492
493     view = malloc (sizeof (view_t));
494     if (view == NULL) {
495         fprintf (stderr, "Out of memory.\n");
496         exit (1);
497     }
498
499     game->num_views++;
500     game->views = realloc (game->views,
501                            game->num_views * sizeof (view_t *));
502     if (game->views == NULL) {
503         fprintf (stderr, "Out of memory.\n");
504         exit (1);
505     }
506     game->views[game->num_views - 1] = view;
507
508     view_init (view, game, window);
509
510     gtk_window_set_default_size (GTK_WINDOW (window), 780, 251);
511
512     g_signal_connect (window, "delete-event",
513                       G_CALLBACK (on_delete_event_quit), NULL);
514
515     drawing_area = gtk_drawing_area_new ();
516
517     gtk_container_add (GTK_CONTAINER (window), drawing_area);
518
519     g_signal_connect (drawing_area, "expose-event",  
520                       G_CALLBACK (on_expose_event_draw), view);
521
522     gtk_widget_add_events (drawing_area, GDK_BUTTON_PRESS_MASK);
523     g_signal_connect (drawing_area, "button-press-event",
524                       G_CALLBACK (on_button_press_event), view);
525
526     gtk_widget_show_all (window);
527
528     return window;
529 }
530
531 int
532 main (int argc, char *argv[])
533 {
534     GtkWidget *window0, *window1;
535     GdkDisplay *display;
536     GdkScreen *screen;
537     dvonn_game_t game;
538
539     dvonn_game_init (&game);
540
541     gtk_init (&argc, &argv);
542
543     /* Create a view for player 1. */
544     window0 = dvonn_game_create_view (&game);
545
546     /* Ugly little hack to get Xauthority data from keithp. Obviously
547      * won't work for any other user.
548      *
549     setenv ("XAUTHORITY", "/home/keithp/.Xauthority", 1);
550     */
551
552     /* Also ugly that localhost:10.0 is hard-coded, but this will work
553      * for many situations--both for when keithp attaches to my
554      * machine, and for when I connect to anyone's machine and then
555      * connect back, (assuming I haven't made other X forwardings
556      * first).
557      *
558      * Clearly we'll want some actual UI to select the right thing
559      * here.
560      */
561     display = gdk_display_open ("localhost:10.0");
562     if (display) {
563         screen = gdk_display_get_default_screen (display);
564         window1 = dvonn_game_create_view (&game);
565         gtk_window_set_screen (GTK_WINDOW (window1), screen);
566
567         game.dual_window_mode = 1;
568         game.windows[0] = window0;
569         game.windows[1] = window1;
570     }
571     
572     gtk_main ();
573
574     return 0;
575 }