]> git.cworth.org Git - dvonn/blobdiff - dvonn.c
Implement automatic pass (when there is no legal move) and end-of-game
[dvonn] / dvonn.c
diff --git a/dvonn.c b/dvonn.c
index 1744770fc8e93404a80ee03e4e8593770593a22d..c1762ed19be159f67832ce71fe032e85e0331ee0 100644 (file)
--- a/dvonn.c
+++ b/dvonn.c
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008 Carl Worth
+ * Copyright (C) 2009 Carl Worth
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * Author: Carl Worth <cworth@cworth.org>
  */
 
+#define _GNU_SOURCE /* for vasprintf */
+#include <stdio.h>
 #include <stdlib.h>
+#include <stdarg.h>
 #include <gtk/gtk.h>
 #include <math.h>
 
@@ -34,15 +37,15 @@ typedef struct {
     int cell_size;
 } layout_t;
 
-typedef struct _loa_game loa_game_t;
+typedef struct _dvonn_game dvonn_game_t;
 
 typedef struct {
-    loa_game_t *game;
+    dvonn_game_t *game;
     GtkWidget *window;
     layout_t layout;
 } view_t;
 
-struct _loa_game {
+struct _dvonn_game {
     view_t **views;
     int num_views;
 
@@ -50,6 +53,12 @@ struct _loa_game {
     dvonn_bool_t has_selected;
     int selected_x;
     int selected_y;
+
+    PangoFontDescription *font;
+    PangoFontDescription *ring_font;
+
+    dvonn_bool_t dual_window_mode;
+    GtkWidget *windows[2];
 };
 
 static gboolean
@@ -66,13 +75,20 @@ on_delete_event_quit (GtkWidget  *widget,
 
 /* Something like buff */
 #define BACKGROUND_COLOR 0.89, 0.70, 0.40
-#define LIGHT_SQUARE_COLOR 0.89, 0.70, 0.40
-/* Something like mahogany */
-#define DARK_SQUARE_COLOR  0.26, 0.02, 0.01
+
+#define RED_RING_COLOR 0.8, 0.2, 0.2
+
+#define DVONN_FONT "sans"
+#define DVONN_FONT_SIZE 12
+
+/* Relative to a unit square. */
+#define RING_OUTER_RADIUS 0.4
+#define RING_INNER_RADIUS 0.2
+#define RING_FONT_SIZE (RING_INNER_RADIUS * 1.5)
 
 /* XXX: This really should have an interest rectangle. */
 static void
-loa_game_update_windows (loa_game_t *game)
+dvonn_game_update_windows (dvonn_game_t *game)
 {
     int i;
 
@@ -80,6 +96,63 @@ loa_game_update_windows (loa_game_t *game)
        gtk_widget_queue_draw (game->views[i]->window);
 }
 
+/* Convert from a board index to a device-pixel coordinate pair, (at
+ * the center of the ring). */
+static void
+layout_board_to_device (layout_t *layout, double *x_ret, double *y_ret)
+{
+    double x = *x_ret, y = *y_ret;
+
+    *x_ret = layout->x_offset + layout->cell_size *
+       (x + (y - DVONN_BOARD_Y_SIZE/2)/2.0 + 0.5);
+    *y_ret = layout->y_offset + layout->cell_size * (M_SQRT1_2 * y + 0.5);
+}
+
+/* Convert from a device-pixel coordinate pair to a board index. */
+static void
+layout_device_to_board (layout_t *layout, int *x_ret, int *y_ret)
+{
+    int x = *x_ret, y = *y_ret;
+    int x1, y1, x2, y2;
+    double x1_dev, y1_dev, x2_dev, y2_dev;
+    double dx, dy, d1, d2;
+
+    /* Because the vertical spacing between adjacent rows is less than
+     * layout->cell_size, the simple calculations here give us two
+     * candidate (x,y) pairs. We then choose the correct one based on
+     * a distance calculation.
+     */
+    y1 = (y - layout->y_offset) / (layout->cell_size * M_SQRT1_2);
+    x1 = (double) (x - layout->x_offset) / layout->cell_size - (y1 - DVONN_BOARD_Y_SIZE/2)/2.0;
+
+    y2 = y1 - 1;
+    x2 = (double) (x - layout->x_offset) / layout->cell_size - (y2 - DVONN_BOARD_Y_SIZE/2)/2.0;
+
+    x1_dev = x1;
+    y1_dev = y1;
+    layout_board_to_device (layout, &x1_dev, &y1_dev);
+
+    x2_dev = x2;
+    y2_dev = y2;
+    layout_board_to_device (layout, &x2_dev, &y2_dev);
+
+    dx = x - x1_dev;
+    dy = y - y1_dev;
+    d1 = sqrt (dx*dx + dy*dy);
+
+    dx = x - x2_dev;
+    dy = y - y2_dev;
+    d2 = sqrt (dx*dx + dy*dy);
+
+    if (d1 < d2) {
+       *x_ret = x1;
+       *y_ret = y1;
+    } else {
+       *x_ret = x2;
+       *y_ret = y2;
+    }
+}
+
 static gboolean
 on_button_press_event (GtkWidget       *widget,
                       GdkEventButton   *event,
@@ -87,23 +160,27 @@ on_button_press_event (GtkWidget   *widget,
 {
     view_t *view = user_data;
     layout_t *layout = &view->layout;
-    loa_game_t *game = view->game;
+    dvonn_game_t *game = view->game;
     int x, y;
     char *error;
 
-    x = (event->x - layout->x_offset) / layout->cell_size;
-    y = (event->y - layout->y_offset) / layout->cell_size;
-
-    if (! game->has_selected) {
-       if (game->board.cells[x][y].type == game->board.player) {
-           game->has_selected = TRUE;
-           game->selected_x = x;
-           game->selected_y = y;
-           loa_game_update_windows (game);
-       }
+    /* Ignore events from the non-player. (Obviously when we add more
+     * interaction abilities, we will want to allow those even for the
+     * non-player---things like quit, etc.). */
+    if (game->dual_window_mode &&
+       widget->parent != game->windows[game->board.player])
+    {
        return TRUE;
     }
 
+    /* Ignore double and triple clicks. */
+    if (event->type >= GDK_2BUTTON_PRESS)
+       return TRUE;
+
+    x = event->x;
+    y = event->y;
+    layout_device_to_board (layout, &x, &y);
+
     /* Do nothing for out-of-bounds clicks */
     if (x < 0 || x >= DVONN_BOARD_X_SIZE ||
        y < 0 || y >= DVONN_BOARD_Y_SIZE)
@@ -111,27 +188,54 @@ on_button_press_event (GtkWidget  *widget,
        return TRUE;
     }
 
-    if (x == game->selected_x &&
-       y == game->selected_y)
+    /* Nor for cells which have array entries that are invalid. */
+    if (game->board.cells[x][y].type == DVONN_CELL_INVALID)
+       return TRUE;
+
+    if (game->board.phase == DVONN_PHASE_PLACEMENT) {
+       if (dvonn_board_place (&game->board, x, y, &error))
+           dvonn_game_update_windows (game);
+       else
+           printf ("Illegal placement %c%d: %s\n",
+                   'a' + x,
+                   y + 1,
+                   error);
+
+       return TRUE;
+    }
+
+    if (! game->has_selected) {
+       if (dvonn_board_cell_owned_by (&game->board, x, y, game->board.player) &&
+           ! dvonn_board_cell_surrounded (&game->board, x, y))
+           {
+                   game->has_selected = TRUE;
+                   game->selected_x = x;
+                   game->selected_y = y;
+                   dvonn_game_update_windows (game);
+           }
+       return TRUE;
+    }
+
+    if (x == game->selected_x && y == game->selected_y)
     {
        game->has_selected = FALSE;
-       loa_game_update_windows (game);
+       dvonn_game_update_windows (game);
        return TRUE;
     }
-       
+
     if (dvonn_board_move (&game->board,
                          game->selected_x, game->selected_y,
                          x, y, &error))
     {
        game->has_selected = FALSE;
-       loa_game_update_windows (game);
+       dvonn_game_update_windows (game);
        return TRUE;
     } else {
        printf ("Illegal move %c%d%c%d: %s\n",
                'a' + game->selected_x,
-               DVONN_BOARD_Y_SIZE - game->selected_y,
+               game->selected_y + 1,
                'a' + x,
-               DVONN_BOARD_Y_SIZE - y,
+               y + 1,
                error);
     }
 
@@ -142,8 +246,73 @@ on_button_press_event (GtkWidget   *widget,
 static void
 ring_path (cairo_t *cr)
 {
-    cairo_arc (cr, 0.5, 0.5, 0.4, 0, 2 * M_PI);
-    cairo_arc_negative (cr, 0.5, 0.5, 0.2, 2 * M_PI, 0);
+    cairo_new_sub_path (cr);
+    cairo_arc (cr, 0.5, 0.5, RING_OUTER_RADIUS, 0, 2 * M_PI);
+    cairo_arc_negative (cr, 0.5, 0.5, RING_INNER_RADIUS, 2 * M_PI, 0);
+}
+
+/* Some helper functions for using pango. */
+static PangoLayout *
+_create_layout (cairo_t *cr, PangoFontDescription *font, const char *text)
+{
+    PangoLayout *layout;
+
+    layout = pango_cairo_create_layout (cr);
+    pango_layout_set_font_description (layout, font);
+    pango_layout_set_text (layout, text, -1);
+    pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER);
+
+    return layout;
+}
+
+#define PRINTF_FORMAT(fmt_index, va_index) __attribute__ ((__format__(__printf__, fmt_index, va_index)))
+
+static PangoLayout *
+_create_layout_vprintf (cairo_t *cr, PangoFontDescription *font, const char *fmt, va_list ap)
+{
+    PangoLayout *layout;
+    char *text;
+
+    vasprintf (&text, fmt, ap);
+
+    layout = _create_layout (cr, font, text);
+
+    free (text);
+
+    return layout;
+}
+
+static PangoLayout *
+_create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
+    PRINTF_FORMAT (3, 4);
+
+static PangoLayout *
+_create_layout_printf (cairo_t *cr, PangoFontDescription *font, const char *fmt, ...)
+{
+    va_list ap;
+    PangoLayout *layout;
+
+    va_start (ap, fmt);
+
+    layout = _create_layout_vprintf (cr, font, fmt, ap);
+
+    va_end (ap);
+
+    return layout;
+}
+
+static void
+_destroy_layout (PangoLayout *layout)
+{
+    g_object_unref (layout);
+}
+
+static void
+_show_layout (cairo_t *cr, PangoLayout *layout)
+{
+    pango_cairo_show_layout (cr, layout);
+
+    _destroy_layout (layout);
 }
 
 static gboolean
@@ -153,9 +322,10 @@ on_expose_event_draw (GtkWidget            *widget,
 {
     view_t *view = user_data;
     layout_t *layout = &view->layout;
-    loa_game_t *game = view->game;
+    dvonn_game_t *game = view->game;
     cairo_t *cr;
     int x, y;
+    PangoLayout *to_move;
 
     if (layout->width != widget->allocation.width ||
        layout->height != widget->allocation.height)
@@ -166,23 +336,76 @@ on_expose_event_draw (GtkWidget           *widget,
        layout->height = widget->allocation.height;
 
        x_size = layout->width;
-       if (x_size > layout->height * BOARD_X_SIZE / BOARD_Y_SIZE)
-           x_size = layout->height * BOARD_X_SIZE / BOARD_Y_SIZE;
+       if (x_size > layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1)))
+           x_size = layout->height * BOARD_X_SIZE / (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
 
        /* Size must be a multiple of the integer cell_size */
        layout->cell_size = x_size / BOARD_X_SIZE;
        x_size = layout->cell_size * BOARD_X_SIZE;
-       y_size = layout->cell_size * BOARD_Y_SIZE;
+       y_size = layout->cell_size * (1 + M_SQRT1_2 * (BOARD_Y_SIZE-1));
 
        layout->x_offset = (layout->width - x_size) / 2;
        layout->y_offset = (layout->height - y_size) / 2;
+
+       if (game->ring_font == NULL) {
+           game->ring_font = pango_font_description_new ();
+           pango_font_description_set_family (game->ring_font,
+                                              DVONN_FONT);
+       }
+       pango_font_description_set_absolute_size (game->ring_font,
+                                                 RING_FONT_SIZE * PANGO_SCALE);
     }
 
     cr = gdk_cairo_create (widget->window);
-    
+
     cairo_set_source_rgb (cr, BACKGROUND_COLOR);
     cairo_paint (cr);
 
+    if (game->font == NULL) {
+       game->font = pango_font_description_new ();
+       pango_font_description_set_family (game->font, DVONN_FONT);
+       pango_font_description_set_absolute_size (game->font, DVONN_FONT_SIZE * PANGO_SCALE);
+    }
+    if (game->board.phase == DVONN_PHASE_GAME_OVER) {
+       if (game->board.score[DVONN_PLAYER_WHITE] >
+           game->board.score[DVONN_PLAYER_BLACK])
+       {
+           to_move = _create_layout_printf (cr, game->font,
+                                            "White wins (%d to %d)\n",
+                                            game->board.score[DVONN_PLAYER_WHITE],
+                                            game->board.score[DVONN_PLAYER_BLACK]);
+       }
+       else if  (game->board.score[DVONN_PLAYER_BLACK] >
+                 game->board.score[DVONN_PLAYER_WHITE])
+       {
+           to_move = _create_layout_printf (cr, game->font,
+                                            "Black wins (%d to %d)\n",
+                                            game->board.score[DVONN_PLAYER_BLACK],
+                                            game->board.score[DVONN_PLAYER_WHITE]);
+       }
+       else
+       {
+           to_move = _create_layout_printf (cr, game->font,
+                                            "Tie game (%d to %d)\n",
+                                            game->board.score[DVONN_PLAYER_WHITE],
+                                            game->board.score[DVONN_PLAYER_BLACK]);
+
+       }
+    } else {
+       to_move = _create_layout_printf (cr, game->font,
+                                        "%s to %s.",
+                                        game->board.player == DVONN_PLAYER_WHITE ?
+                                        "White" : "Black",
+                                        game->board.phase == DVONN_PHASE_PLACEMENT ?
+                                        "place" : "move");
+    }
+    cairo_move_to (cr, 2, 2);
+    if (game->board.player == DVONN_PLAYER_WHITE)
+       cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
+    else
+       cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
+    _show_layout (cr, to_move);
+
     cairo_translate (cr, layout->x_offset, layout->y_offset);
     cairo_scale (cr, layout->cell_size, layout->cell_size);
 
@@ -196,11 +419,51 @@ on_expose_event_draw (GtkWidget           *widget,
 
            cairo_save (cr);
            cairo_translate(cr,
-                           x + (y - DVONN_BOARD_Y_SIZE/2) / 2.0, y);
+                           x + (y - DVONN_BOARD_Y_SIZE/2) / 2.0,
+                           M_SQRT1_2 * y);
+           if (cell.contains_red && cell.type != DVONN_CELL_RED) {
+               cairo_new_sub_path (cr);
+               cairo_arc (cr, 0.5, 0.5,
+                          (RING_INNER_RADIUS + RING_OUTER_RADIUS)/2.0,
+                          0, 2 * M_PI);
+               cairo_set_source_rgb (cr, RED_RING_COLOR);
+               cairo_fill (cr);
+           }
            ring_path (cr);
-           cairo_set_source_rgba (cr, 0.0, 0.0, 0.2, 0.1);
+           switch (cell.type) {
+           case DVONN_CELL_WHITE:
+               cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); /* white */
+               break;
+           case DVONN_CELL_BLACK:
+               cairo_set_source_rgb (cr, 0.0, 0.0, 0.0); /* black */
+               break;
+           case DVONN_CELL_RED:
+               cairo_set_source_rgb (cr, RED_RING_COLOR);
+               break;
+           case DVONN_CELL_EMPTY:
+           default:
+               cairo_set_source_rgba (cr, 0.0, 0.0, 0.2, 0.1);
+               break;
+           }
+           if (game->has_selected &&
+               x == game->selected_x &&
+               y == game->selected_y)
+           {
+               cairo_fill_preserve (cr);
+               cairo_set_source_rgba (cr, 0.2, 0.2, 1.0, 0.4);
+           }
            cairo_fill (cr);
 
+           if (game->board.cells[x][y].height > 1) {
+               PangoLayout *height;
+               cairo_move_to (cr,
+                              0.5 - 0.7 * RING_INNER_RADIUS * cos (M_PI_4),
+                              0.5 - 1.2 * RING_INNER_RADIUS * sin (M_PI_4));
+               height = _create_layout_printf (cr, game->ring_font, "%d",
+                                               game->board.cells[x][y].height);
+               _show_layout (cr, height);
+           }
+
            cairo_restore (cr);
        }
     }
@@ -211,7 +474,7 @@ on_expose_event_draw (GtkWidget             *widget,
 }
 
 static void
-loa_game_init (loa_game_t *game)
+dvonn_game_init (dvonn_game_t *game)
 {
     game->views = NULL;
     game->num_views = 0;
@@ -219,10 +482,17 @@ loa_game_init (loa_game_t *game)
     game->has_selected = FALSE;
 
     dvonn_board_init (&game->board);
+
+    game->font = NULL;
+    game->ring_font = NULL;
+
+    game->dual_window_mode = 0;
+    game->windows[0] = NULL;
+    game->windows[1] = NULL;
 }
 
 static void
-view_init (view_t *view, loa_game_t *game, GtkWidget *window)
+view_init (view_t *view, dvonn_game_t *game, GtkWidget *window)
 {
     view->game = game;
     view->window = window;
@@ -231,8 +501,8 @@ view_init (view_t *view, loa_game_t *game, GtkWidget *window)
     view->layout.height = 0;
 }
 
-static void
-loa_game_create_view (loa_game_t *game)
+static GtkWidget*
+dvonn_game_create_view (dvonn_game_t *game)
 {
     view_t *view;
     GtkWidget *window;
@@ -257,7 +527,7 @@ loa_game_create_view (loa_game_t *game)
 
     view_init (view, game, window);
 
-    gtk_window_set_default_size (GTK_WINDOW (window), 561, 255);
+    gtk_window_set_default_size (GTK_WINDOW (window), 780, 251);
 
     g_signal_connect (window, "delete-event",
                      G_CALLBACK (on_delete_event_quit), NULL);
@@ -274,20 +544,50 @@ loa_game_create_view (loa_game_t *game)
                      G_CALLBACK (on_button_press_event), view);
 
     gtk_widget_show_all (window);
+
+    return window;
 }
 
 int
 main (int argc, char *argv[])
 {
-    loa_game_t game;
+    GtkWidget *window0, *window1;
+    GdkDisplay *display;
+    GdkScreen *screen;
+    dvonn_game_t game;
 
-    loa_game_init (&game);
+    dvonn_game_init (&game);
 
     gtk_init (&argc, &argv);
 
-    /* Create two views of the game (one for each player) */
-    loa_game_create_view (&game);
-    loa_game_create_view (&game);
+    /* Create a view for player 1. */
+    window0 = dvonn_game_create_view (&game);
+
+    /* Ugly little hack to get Xauthority data from keithp. Obviously
+     * won't work for any other user.
+     *
+    setenv ("XAUTHORITY", "/home/keithp/.Xauthority", 1);
+    */
+
+    /* Also ugly that localhost:10.0 is hard-coded, but this will work
+     * for many situations--both for when keithp attaches to my
+     * machine, and for when I connect to anyone's machine and then
+     * connect back, (assuming I haven't made other X forwardings
+     * first).
+     *
+     * Clearly we'll want some actual UI to select the right thing
+     * here.
+     */
+    display = gdk_display_open ("localhost:10.0");
+    if (display) {
+       screen = gdk_display_get_default_screen (display);
+       window1 = dvonn_game_create_view (&game);
+       gtk_window_set_screen (GTK_WINDOW (window1), screen);
+
+       game.dual_window_mode = 1;
+       game.windows[0] = window0;
+       game.windows[1] = window1;
+    }
     
     gtk_main ();