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