]> git.cworth.org Git - dvonn/blob - dvonn.c
Implement automatic pass (when there is no legal move) and end-of-game
[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     x = event->x;
181     y = event->y;
182     layout_device_to_board (layout, &x, &y);
183
184     /* Do nothing for out-of-bounds clicks */
185     if (x < 0 || x >= DVONN_BOARD_X_SIZE ||
186         y < 0 || y >= DVONN_BOARD_Y_SIZE)
187     {
188         return TRUE;
189     }
190
191     /* Nor for cells which have array entries that are invalid. */
192     if (game->board.cells[x][y].type == DVONN_CELL_INVALID)
193         return TRUE;
194
195     if (game->board.phase == DVONN_PHASE_PLACEMENT) {
196         if (dvonn_board_place (&game->board, x, y, &error))
197             dvonn_game_update_windows (game);
198         else
199             printf ("Illegal placement %c%d: %s\n",
200                     'a' + x,
201                     y + 1,
202                     error);
203
204         return TRUE;
205     }
206
207     if (! game->has_selected) {
208         if (dvonn_board_cell_owned_by (&game->board, x, y, game->board.player) &&
209             ! dvonn_board_cell_surrounded (&game->board, x, y))
210             {
211                     game->has_selected = TRUE;
212                     game->selected_x = x;
213                     game->selected_y = y;
214                     dvonn_game_update_windows (game);
215             }
216         return TRUE;
217     }
218
219     if (x == game->selected_x && y == game->selected_y)
220     {
221         game->has_selected = FALSE;
222         dvonn_game_update_windows (game);
223         return TRUE;
224     }
225
226     if (dvonn_board_move (&game->board,
227                           game->selected_x, game->selected_y,
228                           x, y, &error))
229     {
230         game->has_selected = FALSE;
231         dvonn_game_update_windows (game);
232         return TRUE;
233     } else {
234         printf ("Illegal move %c%d%c%d: %s\n",
235                 'a' + game->selected_x,
236                 game->selected_y + 1,
237                 'a' + x,
238                 y + 1,
239                 error);
240     }
241
242     return TRUE;
243 }
244
245 /* Add a unit-sized DVONN-ring path to cr, from (0,0) to (1,1). */
246 static void
247 ring_path (cairo_t *cr)
248 {
249     cairo_new_sub_path (cr);
250     cairo_arc (cr, 0.5, 0.5, RING_OUTER_RADIUS, 0, 2 * M_PI);
251     cairo_arc_negative (cr, 0.5, 0.5, RING_INNER_RADIUS, 2 * M_PI, 0);
252 }
253
254 /* Some helper functions for using pango. */
255 static PangoLayout *
256 _create_layout (cairo_t *cr, PangoFontDescription *font, const char *text)
257 {
258     PangoLayout *layout;
259
260     layout = pango_cairo_create_layout (cr);
261     pango_layout_set_font_description (layout, font);
262     pango_layout_set_text (layout, text, -1);
263     pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER);
264
265     return layout;
266 }
267
268 #define PRINTF_FORMAT(fmt_index, va_index) __attribute__ ((__format__(__printf__, fmt_index, va_index)))
269
270 static PangoLayout *
271 _create_layout_vprintf (cairo_t *cr, PangoFontDescription *font, const char *fmt, va_list ap)
272 {
273     PangoLayout *layout;
274     char *text;
275
276     vasprintf (&text, fmt, ap);
277
278     layout = _create_layout (cr, font, text);
279
280     free (text);
281
282     return layout;
283 }
284
285 static PangoLayout *
286 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
287     PRINTF_FORMAT (3, 4);
288
289 static PangoLayout *
290 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
291 {
292     va_list ap;
293     PangoLayout *layout;
294
295     va_start (ap, fmt);
296
297     layout = _create_layout_vprintf (cr, font, fmt, ap);
298
299     va_end (ap);
300
301     return layout;
302 }
303
304 static void
305 _destroy_layout (PangoLayout *layout)
306 {
307     g_object_unref (layout);
308 }
309
310 static void
311 _show_layout (cairo_t *cr, PangoLayout *layout)
312 {
313     pango_cairo_show_layout (cr, layout);
314
315     _destroy_layout (layout);
316 }
317
318 static gboolean
319 on_expose_event_draw (GtkWidget         *widget,
320                       GdkEventExpose    *event,
321                       gpointer           user_data)
322 {
323     view_t *view = user_data;
324     layout_t *layout = &view->layout;
325     dvonn_game_t *game = view->game;
326     cairo_t *cr;
327     int x, y;
328     PangoLayout *to_move;
329
330     if (layout->width != widget->allocation.width ||
331         layout->height != widget->allocation.height)
332     {
333         int x_size, y_size;
334
335         layout->width = widget->allocation.width;
336         layout->height = widget->allocation.height;
337
338         x_size = layout->width;
339         if (x_size > layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1)))
340             x_size = layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
341
342         /* Size must be a multiple of the integer cell_size */
343         layout->cell_size = x_size / BOARD_X_SIZE;
344         x_size = layout->cell_size * BOARD_X_SIZE;
345         y_size = layout->cell_size * (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
346
347         layout->x_offset = (layout->width - x_size) / 2;
348         layout->y_offset = (layout->height - y_size) / 2;
349
350         if (game->ring_font == NULL) {
351             game->ring_font = pango_font_description_new ();
352             pango_font_description_set_family (game->ring_font,
353                                                DVONN_FONT);
354         }
355         pango_font_description_set_absolute_size (game->ring_font,
356                                                   RING_FONT_SIZE * PANGO_SCALE);
357     }
358
359     cr = gdk_cairo_create (widget->window);
360
361     cairo_set_source_rgb (cr, BACKGROUND_COLOR);
362     cairo_paint (cr);
363
364     if (game->font == NULL) {
365         game->font = pango_font_description_new ();
366         pango_font_description_set_family (game->font, DVONN_FONT);
367         pango_font_description_set_absolute_size (game->font, DVONN_FONT_SIZE * PANGO_SCALE);
368     }
369     if (game->board.phase == DVONN_PHASE_GAME_OVER) {
370         if (game->board.score[DVONN_PLAYER_WHITE] >
371             game->board.score[DVONN_PLAYER_BLACK])
372         {
373             to_move = _create_layout_printf (cr, game->font,
374                                              "White wins (%d to %d)\n",
375                                              game->board.score[DVONN_PLAYER_WHITE],
376                                              game->board.score[DVONN_PLAYER_BLACK]);
377         }
378         else if  (game->board.score[DVONN_PLAYER_BLACK] >
379                   game->board.score[DVONN_PLAYER_WHITE])
380         {
381             to_move = _create_layout_printf (cr, game->font,
382                                              "Black wins (%d to %d)\n",
383                                              game->board.score[DVONN_PLAYER_BLACK],
384                                              game->board.score[DVONN_PLAYER_WHITE]);
385         }
386         else
387         {
388             to_move = _create_layout_printf (cr, game->font,
389                                              "Tie game (%d to %d)\n",
390                                              game->board.score[DVONN_PLAYER_WHITE],
391                                              game->board.score[DVONN_PLAYER_BLACK]);
392
393         }
394     } else {
395         to_move = _create_layout_printf (cr, game->font,
396                                          "%s to %s.",
397                                          game->board.player == DVONN_PLAYER_WHITE ?
398                                          "White" : "Black",
399                                          game->board.phase == DVONN_PHASE_PLACEMENT ?
400                                          "place" : "move");
401     }
402     cairo_move_to (cr, 2, 2);
403     if (game->board.player == DVONN_PLAYER_WHITE)
404         cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
405     else
406         cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
407     _show_layout (cr, to_move);
408
409     cairo_translate (cr, layout->x_offset, layout->y_offset);
410     cairo_scale (cr, layout->cell_size, layout->cell_size);
411
412     for (y = 0; y < BOARD_Y_SIZE; y++) {
413         for (x = 0; x < BOARD_X_SIZE; x++) {
414             dvonn_cell_t cell;
415
416             cell = game->board.cells[x][y];
417             if (cell.type == DVONN_CELL_INVALID)
418                 continue;
419
420             cairo_save (cr);
421             cairo_translate(cr,
422                             x + (y - DVONN_BOARD_Y_SIZE/2) / 2.0,
423                             M_SQRT1_2 * y);
424             if (cell.contains_red && cell.type != DVONN_CELL_RED) {
425                 cairo_new_sub_path (cr);
426                 cairo_arc (cr, 0.5, 0.5,
427                            (RING_INNER_RADIUS + RING_OUTER_RADIUS)/2.0,
428                            0, 2 * M_PI);
429                 cairo_set_source_rgb (cr, RED_RING_COLOR);
430                 cairo_fill (cr);
431             }
432             ring_path (cr);
433             switch (cell.type) {
434             case DVONN_CELL_WHITE:
435                 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
436                 break;
437             case DVONN_CELL_BLACK:
438                 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
439                 break;
440             case DVONN_CELL_RED:
441                 cairo_set_source_rgb (cr, RED_RING_COLOR);
442                 break;
443             case DVONN_CELL_EMPTY:
444             default:
445                 cairo_set_source_rgba (cr, 0.0, 0.0, 0.2, 0.1);
446                 break;
447             }
448             if (game->has_selected &&
449                 x == game->selected_x &&
450                 y == game->selected_y)
451             {
452                 cairo_fill_preserve (cr);
453                 cairo_set_source_rgba (cr, 0.2, 0.2, 1.0, 0.4);
454             }
455             cairo_fill (cr);
456
457             if (game->board.cells[x][y].height > 1) {
458                 PangoLayout *height;
459                 cairo_move_to (cr,
460                                0.5 - 0.7 * RING_INNER_RADIUS * cos (M_PI_4),
461                                0.5 - 1.2 * RING_INNER_RADIUS * sin (M_PI_4));
462                 height = _create_layout_printf (cr, game->ring_font, "%d",
463                                                 game->board.cells[x][y].height);
464                 _show_layout (cr, height);
465             }
466
467             cairo_restore (cr);
468         }
469     }
470
471     cairo_destroy (cr);
472
473     return TRUE;
474 }
475
476 static void
477 dvonn_game_init (dvonn_game_t *game)
478 {
479     game->views = NULL;
480     game->num_views = 0;
481
482     game->has_selected = FALSE;
483
484     dvonn_board_init (&game->board);
485
486     game->font = NULL;
487     game->ring_font = NULL;
488
489     game->dual_window_mode = 0;
490     game->windows[0] = NULL;
491     game->windows[1] = NULL;
492 }
493
494 static void
495 view_init (view_t *view, dvonn_game_t *game, GtkWidget *window)
496 {
497     view->game = game;
498     view->window = window;
499
500     view->layout.width = 0;
501     view->layout.height = 0;
502 }
503
504 static GtkWidget*
505 dvonn_game_create_view (dvonn_game_t *game)
506 {
507     view_t *view;
508     GtkWidget *window;
509     GtkWidget *drawing_area;
510
511     window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
512
513     view = malloc (sizeof (view_t));
514     if (view == NULL) {
515         fprintf (stderr, "Out of memory.\n");
516         exit (1);
517     }
518
519     game->num_views++;
520     game->views = realloc (game->views,
521                            game->num_views * sizeof (view_t *));
522     if (game->views == NULL) {
523         fprintf (stderr, "Out of memory.\n");
524         exit (1);
525     }
526     game->views[game->num_views - 1] = view;
527
528     view_init (view, game, window);
529
530     gtk_window_set_default_size (GTK_WINDOW (window), 780, 251);
531
532     g_signal_connect (window, "delete-event",
533                       G_CALLBACK (on_delete_event_quit), NULL);
534
535     drawing_area = gtk_drawing_area_new ();
536
537     gtk_container_add (GTK_CONTAINER (window), drawing_area);
538
539     g_signal_connect (drawing_area, "expose-event",  
540                       G_CALLBACK (on_expose_event_draw), view);
541
542     gtk_widget_add_events (drawing_area, GDK_BUTTON_PRESS_MASK);
543     g_signal_connect (drawing_area, "button-press-event",
544                       G_CALLBACK (on_button_press_event), view);
545
546     gtk_widget_show_all (window);
547
548     return window;
549 }
550
551 int
552 main (int argc, char *argv[])
553 {
554     GtkWidget *window0, *window1;
555     GdkDisplay *display;
556     GdkScreen *screen;
557     dvonn_game_t game;
558
559     dvonn_game_init (&game);
560
561     gtk_init (&argc, &argv);
562
563     /* Create a view for player 1. */
564     window0 = dvonn_game_create_view (&game);
565
566     /* Ugly little hack to get Xauthority data from keithp. Obviously
567      * won't work for any other user.
568      *
569     setenv ("XAUTHORITY", "/home/keithp/.Xauthority", 1);
570     */
571
572     /* Also ugly that localhost:10.0 is hard-coded, but this will work
573      * for many situations--both for when keithp attaches to my
574      * machine, and for when I connect to anyone's machine and then
575      * connect back, (assuming I haven't made other X forwardings
576      * first).
577      *
578      * Clearly we'll want some actual UI to select the right thing
579      * here.
580      */
581     display = gdk_display_open ("localhost:10.0");
582     if (display) {
583         screen = gdk_display_get_default_screen (display);
584         window1 = dvonn_game_create_view (&game);
585         gtk_window_set_screen (GTK_WINDOW (window1), screen);
586
587         game.dual_window_mode = 1;
588         game.windows[0] = window0;
589         game.windows[1] = window1;
590     }
591     
592     gtk_main ();
593
594     return 0;
595 }