2 * Copyright (C) 2009 Carl Worth
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.
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.
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/ .
17 * Author: Carl Worth <cworth@cworth.org>
20 #define _GNU_SOURCE /* for vasprintf */
27 #include "dvonn-board.h"
29 #define BOARD_X_SIZE 11
30 #define BOARD_Y_SIZE 5
40 typedef struct _dvonn_game dvonn_game_t;
53 dvonn_bool_t has_selected;
57 PangoFontDescription *font;
58 PangoFontDescription *ring_font;
60 dvonn_bool_t dual_window_mode;
61 GtkWidget *windows[2];
65 on_delete_event_quit (GtkWidget *widget,
71 /* Returning FALSE allows the default handler for delete-event
72 * to proceed to cleanup the widget. */
76 /* Something like buff */
77 #define BACKGROUND_COLOR 0.89, 0.70, 0.40
79 #define RED_RING_COLOR 0.8, 0.2, 0.2
81 #define DVONN_FONT "sans"
82 #define DVONN_FONT_SIZE 12
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)
89 /* XXX: This really should have an interest rectangle. */
91 dvonn_game_update_windows (dvonn_game_t *game)
95 for (i = 0; i < game->num_views; i++)
96 gtk_widget_queue_draw (game->views[i]->window);
99 /* Convert from a board index to a device-pixel coordinate pair, (at
100 * the center of the ring). */
102 layout_board_to_device (layout_t *layout, double *x_ret, double *y_ret)
104 double x = *x_ret, y = *y_ret;
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);
111 /* Convert from a device-pixel coordinate pair to a board index. */
113 layout_device_to_board (layout_t *layout, int *x_ret, int *y_ret)
115 int x = *x_ret, y = *y_ret;
117 double x1_dev, y1_dev, x2_dev, y2_dev;
118 double dx, dy, d1, d2;
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.
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;
129 x2 = (double) (x - layout->x_offset) / layout->cell_size - (y2 - DVONN_BOARD_Y_SIZE/2)/2.0;
133 layout_board_to_device (layout, &x1_dev, &y1_dev);
137 layout_board_to_device (layout, &x2_dev, &y2_dev);
141 d1 = sqrt (dx*dx + dy*dy);
145 d2 = sqrt (dx*dx + dy*dy);
157 on_button_press_event (GtkWidget *widget,
158 GdkEventButton *event,
161 view_t *view = user_data;
162 layout_t *layout = &view->layout;
163 dvonn_game_t *game = view->game;
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])
176 /* Ignore double and triple clicks. */
177 if (event->type >= GDK_2BUTTON_PRESS)
182 layout_device_to_board (layout, &x, &y);
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)
191 /* Nor for cells which have array entries that are invalid. */
192 if (game->board.cells[x][y].type == DVONN_CELL_INVALID)
195 if (game->board.phase == DVONN_PHASE_PLACEMENT) {
196 if (dvonn_board_place (&game->board, x, y, &error))
197 dvonn_game_update_windows (game);
199 printf ("Illegal placement %c%d: %s\n",
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))
211 game->has_selected = TRUE;
212 game->selected_x = x;
213 game->selected_y = y;
214 dvonn_game_update_windows (game);
219 if (x == game->selected_x && y == game->selected_y)
221 game->has_selected = FALSE;
222 dvonn_game_update_windows (game);
226 if (dvonn_board_move (&game->board,
227 game->selected_x, game->selected_y,
230 game->has_selected = FALSE;
231 dvonn_game_update_windows (game);
234 printf ("Illegal move %c%d%c%d: %s\n",
235 'a' + game->selected_x,
236 game->selected_y + 1,
245 /* Add a unit-sized DVONN-ring path to cr, from (0,0) to (1,1). */
247 ring_path (cairo_t *cr)
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);
254 /* Some helper functions for using pango. */
256 _create_layout (cairo_t *cr, PangoFontDescription *font, const char *text)
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);
268 #define PRINTF_FORMAT(fmt_index, va_index) __attribute__ ((__format__(__printf__, fmt_index, va_index)))
271 _create_layout_vprintf (cairo_t *cr, PangoFontDescription *font, const char *fmt, va_list ap)
276 vasprintf (&text, fmt, ap);
278 layout = _create_layout (cr, font, text);
286 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
287 PRINTF_FORMAT (3, 4);
290 _create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
297 layout = _create_layout_vprintf (cr, font, fmt, ap);
305 _destroy_layout (PangoLayout *layout)
307 g_object_unref (layout);
311 _show_layout (cairo_t *cr, PangoLayout *layout)
313 pango_cairo_show_layout (cr, layout);
315 _destroy_layout (layout);
319 on_expose_event_draw (GtkWidget *widget,
320 GdkEventExpose *event,
323 view_t *view = user_data;
324 layout_t *layout = &view->layout;
325 dvonn_game_t *game = view->game;
328 PangoLayout *to_move;
330 if (layout->width != widget->allocation.width ||
331 layout->height != widget->allocation.height)
335 layout->width = widget->allocation.width;
336 layout->height = widget->allocation.height;
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));
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));
347 layout->x_offset = (layout->width - x_size) / 2;
348 layout->y_offset = (layout->height - y_size) / 2;
350 if (game->ring_font == NULL) {
351 game->ring_font = pango_font_description_new ();
352 pango_font_description_set_family (game->ring_font,
355 pango_font_description_set_absolute_size (game->ring_font,
356 RING_FONT_SIZE * PANGO_SCALE);
359 cr = gdk_cairo_create (widget->window);
361 cairo_set_source_rgb (cr, BACKGROUND_COLOR);
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);
369 if (game->board.phase == DVONN_PHASE_GAME_OVER) {
370 if (game->board.score[DVONN_PLAYER_WHITE] >
371 game->board.score[DVONN_PLAYER_BLACK])
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]);
378 else if (game->board.score[DVONN_PLAYER_BLACK] >
379 game->board.score[DVONN_PLAYER_WHITE])
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]);
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]);
395 to_move = _create_layout_printf (cr, game->font,
397 game->board.player == DVONN_PLAYER_WHITE ?
399 game->board.phase == DVONN_PHASE_PLACEMENT ?
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);
406 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
407 _show_layout (cr, to_move);
409 cairo_translate (cr, layout->x_offset, layout->y_offset);
410 cairo_scale (cr, layout->cell_size, layout->cell_size);
412 for (y = 0; y < BOARD_Y_SIZE; y++) {
413 for (x = 0; x < BOARD_X_SIZE; x++) {
416 cell = game->board.cells[x][y];
417 if (cell.type == DVONN_CELL_INVALID)
422 x + (y - DVONN_BOARD_Y_SIZE/2) / 2.0,
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,
429 cairo_set_source_rgb (cr, RED_RING_COLOR);
434 case DVONN_CELL_WHITE:
435 cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
437 case DVONN_CELL_BLACK:
438 cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
441 cairo_set_source_rgb (cr, RED_RING_COLOR);
443 case DVONN_CELL_EMPTY:
445 cairo_set_source_rgba (cr, 0.0, 0.0, 0.2, 0.1);
448 if (game->has_selected &&
449 x == game->selected_x &&
450 y == game->selected_y)
452 cairo_fill_preserve (cr);
453 cairo_set_source_rgba (cr, 0.2, 0.2, 1.0, 0.4);
457 if (game->board.cells[x][y].height > 1) {
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);
477 dvonn_game_init (dvonn_game_t *game)
482 game->has_selected = FALSE;
484 dvonn_board_init (&game->board);
487 game->ring_font = NULL;
489 game->dual_window_mode = 0;
490 game->windows[0] = NULL;
491 game->windows[1] = NULL;
495 view_init (view_t *view, dvonn_game_t *game, GtkWidget *window)
498 view->window = window;
500 view->layout.width = 0;
501 view->layout.height = 0;
505 dvonn_game_create_view (dvonn_game_t *game)
509 GtkWidget *drawing_area;
511 window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
513 view = malloc (sizeof (view_t));
515 fprintf (stderr, "Out of memory.\n");
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");
526 game->views[game->num_views - 1] = view;
528 view_init (view, game, window);
530 gtk_window_set_default_size (GTK_WINDOW (window), 780, 251);
532 g_signal_connect (window, "delete-event",
533 G_CALLBACK (on_delete_event_quit), NULL);
535 drawing_area = gtk_drawing_area_new ();
537 gtk_container_add (GTK_CONTAINER (window), drawing_area);
539 g_signal_connect (drawing_area, "expose-event",
540 G_CALLBACK (on_expose_event_draw), view);
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);
546 gtk_widget_show_all (window);
552 main (int argc, char *argv[])
554 GtkWidget *window0, *window1;
559 dvonn_game_init (&game);
561 gtk_init (&argc, &argv);
563 /* Create a view for player 1. */
564 window0 = dvonn_game_create_view (&game);
566 /* Ugly little hack to get Xauthority data from keithp. Obviously
567 * won't work for any other user.
569 setenv ("XAUTHORITY", "/home/keithp/.Xauthority", 1);
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
578 * Clearly we'll want some actual UI to select the right thing
581 display = gdk_display_open ("localhost:10.0");
583 screen = gdk_display_get_default_screen (display);
584 window1 = dvonn_game_create_view (&game);
585 gtk_window_set_screen (GTK_WINDOW (window1), screen);
587 game.dual_window_mode = 1;
588 game.windows[0] = window0;
589 game.windows[1] = window1;