From 2ac951e70afa8561599b774ceca6ec8e73d28113 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Wed, 18 Sep 2024 16:09:07 +0100 Subject: [PATCH] Adapt Untangle into a graph editor. This builds a secondary GUI program sharing most of the Untangle code, similar to galaxieseditor. You can still drag points around as in the actual Untangle game, but also you can right-drag to add or remove an edge between two points. And the 'copy to clipboard' action generates the Untangle game id corresponding to whatever you ended up with. This could be used for hand-designing actual Untangle games, but my more immediate use for it is as a convenient method of constructing test cases for the new graph testing code. --- CMakeLists.txt | 2 + untangle.c | 383 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 313 insertions(+), 72 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ce3ce3d..073f663 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -268,6 +268,8 @@ puzzle(untangle DISPLAYNAME "Untangle" DESCRIPTION "Planar graph layout puzzle" OBJECTIVE "Reposition the points so that the lines do not cross.") +guiprogram(grapheditor untangle.c + COMPILE_DEFINITIONS EDITOR) add_subdirectory(unfinished) add_subdirectory(auxiliary) diff --git a/untangle.c b/untangle.c index f0647c8..8db6fb0 100644 --- a/untangle.c +++ b/untangle.c @@ -94,9 +94,11 @@ struct game_state { game_params params; int w, h; /* extent of coordinate system only */ point *pts; - int *crosses; /* mark edges which are crossed */ struct graph *graph; +#ifndef EDITOR + int *crosses; /* mark edges which are crossed */ bool completed, cheated, just_solved; +#endif }; static int edgecmpC(const void *av, const void *bv) @@ -205,13 +207,19 @@ static game_params *custom_params(const config_item *cfg) static const char *validate_params(const game_params *params, bool full) { +#ifndef EDITOR if (params->n < 4) return "Number of points must be at least four"; +#else + if (params->n < 1) + return "Number of points must be at least one"; +#endif if (params->n > INT_MAX / 3) return "Number of points must not be unreasonably large"; return NULL; } +#ifndef EDITOR /* ---------------------------------------------------------------------- * Small number of 64-bit integer arithmetic operations, to prevent * integer overflow at the very core of cross(). @@ -402,6 +410,8 @@ static bool cross(point a1, point a2, point b1, point b2) return true; } +#endif /* EDITOR */ + static unsigned long squarert(unsigned long n) { unsigned long d, a, b, di; @@ -443,6 +453,21 @@ static void addedge(tree234 *edges, int a, int b) sfree(e); } +#ifdef EDITOR +static void deledge(tree234 *edges, int a, int b) +{ + edge e, *found; + + assert(a != b); + + e.a = min(a, b); + e.b = max(a, b); + + found = del234(edges, &e); + sfree(found); +} +#endif + static bool isedge(tree234 *edges, int a, int b) { edge e; @@ -460,6 +485,7 @@ typedef struct vertex { int vindex; } vertex; +#ifndef EDITOR static int vertcmpC(const void *av, const void *bv) { const vertex *a = (const vertex *)av; @@ -476,6 +502,7 @@ static int vertcmpC(const void *av, const void *bv) return 0; } static int vertcmp(void *av, void *bv) { return vertcmpC(av, bv); } +#endif /* * Construct point coordinates for n points arranged in a circle, @@ -511,9 +538,73 @@ static void make_circle(point *pts, int n, int w) } } +/* + * Encode a graph in Untangle's game id: a comma-separated list of + * dash-separated vertex number pairs, numbered from zero. + * + * If params != NULL, then the number of vertices is prefixed to the + * front to make a full Untangle game id. Otherwise, we return just + * the game description part. + * + * If mapping != NULL, then it is expected to be a mapping from the + * graph's original vertex numbers to output vertex numbers. + */ +static char *encode_graph(const game_params *params, tree234 *edges, + const long *mapping) +{ + const char *sep; + char buf[80]; + int i, k, m, retlen; + edge *e, *ea; + char *ret; + + retlen = 0; + if (params) + retlen += sprintf(buf, "%d:", params->n); + + m = count234(edges); + ea = snewn(m, edge); + for (i = 0; (e = index234(edges, i)) != NULL; i++) { + int ma, mb; + assert(i < m); + if (mapping) { + ma = mapping[e->a]; + mb = mapping[e->b]; + } else { + ma = e->a; + mb = e->b; + } + ea[i].a = min(ma, mb); + ea[i].b = max(ma, mb); + if (i > 0) + retlen++; /* comma separator after the previous edge */ + retlen += sprintf(buf, "%d-%d", ea[i].a, ea[i].b); + } + assert(i == m); + /* Re-sort to prevent side channels, if mapping was used */ + qsort(ea, m, sizeof(*ea), edgecmpC); + + ret = snewn(retlen + 1, char); + sep = ""; + k = 0; + if (params) + k += sprintf(ret + k, "%d:", params->n); + + for (i = 0; i < m; i++) { + k += sprintf(ret + k, "%s%d-%d", sep, ea[i].a, ea[i].b); + sep = ","; + } + assert(k == retlen); + + sfree(ea); + + return ret; +} + static char *new_game_desc(const game_params *params, random_state *rs, char **aux, bool interactive) { +#ifndef EDITOR int n = params->n, i; long w, h, j, k, m; point *pts, *pts2; @@ -672,42 +763,9 @@ static char *new_game_desc(const game_params *params, random_state *rs, } /* - * We're done. Now encode the graph in a string format. Let's - * use a comma-separated list of dash-separated vertex number - * pairs, numbered from zero. We'll sort the list to prevent - * side channels. + * We're done. Encode the output graph as a string. */ - ret = NULL; - { - const char *sep; - char buf[80]; - int retlen; - edge *ea; - - retlen = 0; - m = count234(edges); - ea = snewn(m, edge); - for (i = 0; (e = index234(edges, i)) != NULL; i++) { - assert(i < m); - ea[i].a = min(tmp[e->a], tmp[e->b]); - ea[i].b = max(tmp[e->a], tmp[e->b]); - retlen += 1 + sprintf(buf, "%d-%d", ea[i].a, ea[i].b); - } - assert(i == m); - qsort(ea, m, sizeof(*ea), edgecmpC); - - ret = snewn(retlen, char); - sep = ""; - k = 0; - - for (i = 0; i < m; i++) { - k += sprintf(ret + k, "%s%d-%d", sep, ea[i].a, ea[i].b); - sep = ","; - } - assert(k < retlen); - - sfree(ea); - } + ret = encode_graph(NULL, edges, tmp); /* * Encode the solution we started with as an aux_info string. @@ -752,6 +810,9 @@ static char *new_game_desc(const game_params *params, random_state *rs, sfree(pts); return ret; +#else + return dupstr(""); +#endif } static const char *validate_desc(const game_params *params, const char *desc) @@ -782,6 +843,7 @@ static const char *validate_desc(const game_params *params, const char *desc) return NULL; } +#ifndef EDITOR static void mark_crossings(game_state *state) { bool ok = true; @@ -815,6 +877,7 @@ static void mark_crossings(game_state *state) if (ok) state->completed = true; } +#endif static game_state *new_game(midend *me, const game_params *params, const char *desc) @@ -830,7 +893,9 @@ static game_state *new_game(midend *me, const game_params *params, state->graph = snew(struct graph); state->graph->refcount = 1; state->graph->edges = newtree234(edgecmp); +#ifndef EDITOR state->completed = state->cheated = state->just_solved = false; +#endif while (*desc) { a = atoi(desc); @@ -848,8 +913,10 @@ static game_state *new_game(midend *me, const game_params *params, addedge(state->graph->edges, a, b); } +#ifndef EDITOR state->crosses = snewn(count234(state->graph->edges), int); mark_crossings(state); /* sets up `crosses' and `completed' */ +#endif return state; } @@ -864,6 +931,7 @@ static game_state *dup_game(const game_state *state) ret->h = state->h; ret->pts = snewn(n, point); memcpy(ret->pts, state->pts, n * sizeof(point)); +#ifndef EDITOR ret->graph = state->graph; ret->graph->refcount++; ret->completed = state->completed; @@ -872,6 +940,19 @@ static game_state *dup_game(const game_state *state) ret->crosses = snewn(count234(ret->graph->edges), int); memcpy(ret->crosses, state->crosses, count234(ret->graph->edges) * sizeof(int)); +#else + /* For the graph editor, we must clone the whole graph */ + ret->graph = snew(struct graph); + ret->graph->refcount = 1; + ret->graph->edges = newtree234(edgecmp); + { + int i; + struct edge *edge; + for (i = 0; (edge = index234(state->graph->edges, i)) != NULL; i++) { + addedge(ret->graph->edges, edge->a, edge->b); + } + } +#endif return ret; } @@ -889,6 +970,7 @@ static void free_game(game_state *state) sfree(state); } +#ifndef EDITOR static char *solve_game(const game_state *state, const game_state *currstate, const char *aux, const char **error) { @@ -1034,6 +1116,21 @@ static char *solve_game(const game_state *state, const game_state *currstate, return ret; } +#endif /* EDITOR */ + +#ifdef EDITOR +static bool game_can_format_as_text_now(const game_params *params) +{ + return true; +} + +static char *game_text_format(const game_state *state) +{ + return encode_graph(&state->params, state->graph->edges, NULL); +} +#endif /* EDITOR */ + +typedef enum DragType { DRAG_MOVE_POINT, DRAG_TOGGLE_EDGE } DragType; struct game_ui { /* Invariant: at most one of {dragpoint, cursorpoint} may be valid @@ -1048,6 +1145,8 @@ struct game_ui { bool just_moved; /* _set_ in game_changed_state */ float anim_length; + DragType dragtype; + /* * User preference option to snap dragged points to a coarse-ish * grid. Requested by a user who otherwise found themself spending @@ -1196,6 +1295,41 @@ static float normsq(point pt) { return (pt.x * pt.x + pt.y * pt.y) / (pt.d * pt.d); } +/* + * Find a vertex within DRAG_THRESHOLD of the pointer, or return -1 if + * no such point exists. In case of more than one, we return the one + * _nearest_ to the pointer, so that if two points are very close it's + * still possible to drag a specific one of them. + */ +static int point_under_mouse(const game_state *state, + const game_drawstate *ds, int x, int y) +{ + int n = state->params.n; + int i, best; + long bestd; + + best = -1; + bestd = 0; + + for (i = 0; i < n; i++) { + long px = state->pts[i].x * ds->tilesize / state->pts[i].d; + long py = state->pts[i].y * ds->tilesize / state->pts[i].d; + long dx = px - x; + long dy = py - y; + long d = dx*dx + dy*dy; + + if (best == -1 || bestd > d) { + best = i; + bestd = d; + } + } + + if (bestd <= DRAG_THRESHOLD * DRAG_THRESHOLD) + return best; + + return -1; +} + static char *interpret_move(const game_state *state, game_ui *ui, const game_drawstate *ds, int x, int y, int button) @@ -1203,42 +1337,27 @@ static char *interpret_move(const game_state *state, game_ui *ui, int n = state->params.n; if (IS_MOUSE_DOWN(button)) { - int i, best; - long bestd; + int p = point_under_mouse(state, ds, x, y); + if (p >= 0) { + ui->dragtype = DRAG_MOVE_POINT; +#ifdef EDITOR + if (button == RIGHT_BUTTON) + ui->dragtype = DRAG_TOGGLE_EDGE; +#endif - /* - * Begin drag. We drag the vertex _nearest_ to the pointer, - * just in case one is nearly on top of another and we want - * to drag the latter. However, we drag nothing at all if - * the nearest vertex is outside DRAG_THRESHOLD. - */ - best = -1; - bestd = 0; - - for (i = 0; i < n; i++) { - long px = state->pts[i].x * ds->tilesize / state->pts[i].d; - long py = state->pts[i].y * ds->tilesize / state->pts[i].d; - long dx = px - x; - long dy = py - y; - long d = dx*dx + dy*dy; - - if (best == -1 || bestd > d) { - best = i; - bestd = d; - } - } - - if (bestd <= DRAG_THRESHOLD * DRAG_THRESHOLD) { - ui->dragpoint = best; + ui->dragpoint = p; ui->cursorpoint = -1; /* eliminate the cursor point, if any */ - place_dragged_point(state, ui, ds, x, y); + if (ui->dragtype == DRAG_MOVE_POINT) + place_dragged_point(state, ui, ds, x, y); return MOVE_UI_UPDATE; } return MOVE_NO_EFFECT; - } else if (IS_MOUSE_DRAG(button) && ui->dragpoint >= 0) { + } else if (IS_MOUSE_DRAG(button) && ui->dragpoint >= 0 && + ui->dragtype == DRAG_MOVE_POINT) { place_dragged_point(state, ui, ds, x, y); return MOVE_UI_UPDATE; - } else if (IS_MOUSE_RELEASE(button) && ui->dragpoint >= 0) { + } else if (IS_MOUSE_RELEASE(button) && ui->dragpoint >= 0 && + ui->dragtype == DRAG_MOVE_POINT) { int p = ui->dragpoint; char buf[80]; @@ -1263,7 +1382,32 @@ static char *interpret_move(const game_state *state, game_ui *ui, ui->newpoint.x, ui->newpoint.y, ui->newpoint.d); ui->just_dragged = true; return dupstr(buf); - } else if (IS_MOUSE_DRAG(button) || IS_MOUSE_RELEASE(button)) { +#ifdef EDITOR + } else if (IS_MOUSE_DRAG(button) && ui->dragpoint >= 0 && + ui->dragtype == DRAG_TOGGLE_EDGE) { + ui->cursorpoint = point_under_mouse(state, ds, x, y); + return MOVE_UI_UPDATE; + } else if (IS_MOUSE_RELEASE(button) && ui->dragpoint >= 0 && + ui->dragtype == DRAG_TOGGLE_EDGE) { + int p = ui->dragpoint; + int q = point_under_mouse(state, ds, x, y); + char buf[80]; + + ui->dragpoint = -1; /* terminate drag, no matter what */ + ui->cursorpoint = -1; /* also eliminate the cursor point */ + + if (q < 0 || p == q) + return MOVE_UI_UPDATE; + + sprintf(buf, "E%c%d,%d", + isedge(state->graph->edges, p, q) ? 'D' : 'A', + p, q); + return dupstr(buf); +#endif /* EDITOR */ + } else if (IS_MOUSE_DRAG(button)) { + return MOVE_NO_EFFECT; + } else if (IS_MOUSE_RELEASE(button)) { + assert(ui->dragpoint == -1); return MOVE_NO_EFFECT; } else if(IS_CURSOR_MOVE(button)) { @@ -1455,14 +1599,59 @@ static game_state *execute_move(const game_state *state, const char *move) long x, y, d; game_state *ret = dup_game(state); +#ifndef EDITOR ret->just_solved = false; +#endif + +#ifdef EDITOR + if (*move == 'E') { + bool add; + int a, b; + + move++; + if (*move == 'A') + add = true; + else if (*move == 'D') + add = false; + else { + free_game(ret); + return NULL; + } + + move++; + a = atoi(move); + while (*move && isdigit((unsigned char)*move)) + move++; + + if (*move != ',') { + free_game(ret); + return NULL; + } + move++; + + b = atoi(move); + + if (a >= 0 && a < n && b >= 0 && b < n && a != b) { + if (add) + addedge(ret->graph->edges, a, b); + else + deledge(ret->graph->edges, a, b); + return ret; + } else { + free_game(ret); + return NULL; + } + } +#endif while (*move) { +#ifndef EDITOR if (*move == 'S') { move++; if (*move == ';') move++; ret->cheated = ret->just_solved = true; } +#endif if (*move == 'P' && sscanf(move+1, "%d:%ld,%ld/%ld%n", &p, &x, &y, &d, &k) == 4 && p >= 0 && p < n && d > 0) { @@ -1478,7 +1667,9 @@ static game_state *execute_move(const game_state *state, const char *move) } } +#ifndef EDITOR mark_crossings(ret); +#endif return ret; } @@ -1598,6 +1789,9 @@ static void game_redraw(drawing *dr, game_drawstate *ds, int i, j; int bg; bool points_moved; +#ifdef EDITOR + bool edges_changed; +#endif /* * There's no terribly sensible way to do partial redraws of @@ -1627,7 +1821,7 @@ static void game_redraw(drawing *dr, game_drawstate *ds, point p = state->pts[i]; long x, y; - if (ui->dragpoint == i) + if (ui->dragpoint == i && ui->dragtype == DRAG_MOVE_POINT) p = ui->newpoint; if (oldstate) @@ -1643,9 +1837,33 @@ static void game_redraw(drawing *dr, game_drawstate *ds, ds->y[i] = y; } +#ifdef EDITOR + edges_changed = false; + if (oldstate) { + for (i = 0;; i++) { + edge *eold = index234(oldstate->graph->edges, i); + edge *enew = index234(state->graph->edges, i); + if (!eold && !enew) + break; + if (!eold || !enew) { + edges_changed = true; + break; + } + if (eold->a != enew->a || eold->b != enew->b) { + edges_changed = true; + break; + } + } + } +#endif + if (ds->bg == bg && ds->dragpoint == ui->dragpoint && - ds->cursorpoint == ui->cursorpoint && !points_moved) + ds->cursorpoint == ui->cursorpoint && +#ifdef EDITOR + !edges_changed && +#endif + !points_moved) return; /* nothing to do */ ds->dragpoint = ui->dragpoint; @@ -1660,10 +1878,16 @@ static void game_redraw(drawing *dr, game_drawstate *ds, */ for (i = 0; (e = index234(state->graph->edges, i)) != NULL; i++) { +#ifndef EDITOR + int colour = ui->show_crossed_edges && + (oldstate?oldstate:state)->crosses[i] ? + COL_CROSSEDLINE : COL_LINE; +#else + int colour = COL_LINE; +#endif + draw_line(dr, ds->x[e->a], ds->y[e->a], ds->x[e->b], ds->y[e->b], - ui->show_crossed_edges && - (oldstate?oldstate:state)->crosses[i] ? - COL_CROSSEDLINE : COL_LINE); + colour); } /* @@ -1721,19 +1945,25 @@ static float game_anim_length(const game_state *oldstate, { if (ui->just_moved) return 0.0F; +#ifndef EDITOR if ((dir < 0 ? oldstate : newstate)->just_solved) ui->anim_length = SOLVEANIM_TIME; else ui->anim_length = ANIM_TIME; +#else + ui->anim_length = ANIM_TIME; +#endif return ui->anim_length; } static float game_flash_length(const game_state *oldstate, const game_state *newstate, int dir, game_ui *ui) { +#ifndef EDITOR if (!oldstate->completed && newstate->completed && !oldstate->cheated && !newstate->cheated) return FLASH_TIME; +#endif return 0.0F; } @@ -1744,7 +1974,7 @@ static void game_get_cursor_location(const game_ui *ui, int *x, int *y, int *w, int *h) { point pt; - if(ui->dragpoint >= 0) + if (ui->dragpoint >= 0 && ui->dragtype == DRAG_MOVE_POINT) pt = ui->newpoint; else if(ui->cursorpoint >= 0) pt = state->pts[ui->cursorpoint]; @@ -1761,7 +1991,11 @@ static void game_get_cursor_location(const game_ui *ui, static int game_status(const game_state *state) { +#ifdef EDITOR + return 0; +#else return state->completed ? +1 : 0; +#endif } #ifdef COMBINED @@ -1783,8 +2017,13 @@ const struct game thegame = { new_game, dup_game, free_game, +#ifndef EDITOR true, solve_game, false, NULL, NULL, /* can_format_as_text_now, text_format */ +#else + false, NULL, + true, game_can_format_as_text_now, game_text_format, +#endif get_prefs, set_prefs, new_ui, free_ui,