ref: dd0004fb5ebb581eced0820cd6373f88e98d93e3
dir: /pearl.c/
/* * pearl.c: Nikoli's `Masyu' puzzle. */ /* * TODO: * * - The current keyboard cursor mechanism works well on ordinary PC * keyboards, but for platforms with only arrow keys and a select * button or two, we may at some point need a simpler one which can * handle 'x' markings without needing shift keys. For instance, a * cursor with twice the grid resolution, so that it can range * across face centres, edge centres and vertices; 'clicks' on face * centres begin a drag as currently, clicks on edges toggle * markings, and clicks on vertices are ignored (but it would be * too confusing not to let the cursor rest on them). But I'm * pretty sure that would be less pleasant to play on a full * keyboard, so probably a #ifdef would be the thing. * * - Generation is still pretty slow, due to difficulty coming up in * the first place with a loop that makes a soluble puzzle even * with all possible clues filled in. * + A possible alternative strategy to further tuning of the * existing loop generator would be to throw the entire * mechanism out and instead write a different generator from * scratch which evolves the solution along with the puzzle: * place a few clues, nail down a bit of the loop, place another * clue, nail down some more, etc. However, I don't have a * detailed plan for any such mechanism, so it may be a pipe * dream. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include <ctype.h> #include <limits.h> #ifdef NO_TGMATH_H # include <math.h> #else # include <tgmath.h> #endif #include "puzzles.h" #include "grid.h" #include "loopgen.h" #define SWAP(i,j) do { int swaptmp = (i); (i) = (j); (j) = swaptmp; } while (0) #define NOCLUE 0 #define CORNER 1 #define STRAIGHT 2 #define R 1 #define U 2 #define L 4 #define D 8 #define DX(d) ( ((d)==R) - ((d)==L) ) #define DY(d) ( ((d)==D) - ((d)==U) ) #define F(d) (((d << 2) | (d >> 2)) & 0xF) #define C(d) (((d << 3) | (d >> 1)) & 0xF) #define A(d) (((d << 1) | (d >> 3)) & 0xF) #define LR (L | R) #define RL (R | L) #define UD (U | D) #define DU (D | U) #define LU (L | U) #define UL (U | L) #define LD (L | D) #define DL (D | L) #define RU (R | U) #define UR (U | R) #define RD (R | D) #define DR (D | R) #define BLANK 0 #define UNKNOWN 15 #define bLR (1 << LR) #define bRL (1 << RL) #define bUD (1 << UD) #define bDU (1 << DU) #define bLU (1 << LU) #define bUL (1 << UL) #define bLD (1 << LD) #define bDL (1 << DL) #define bRU (1 << RU) #define bUR (1 << UR) #define bRD (1 << RD) #define bDR (1 << DR) #define bBLANK (1 << BLANK) enum { COL_BACKGROUND, COL_HIGHLIGHT, COL_LOWLIGHT, COL_CURSOR_BACKGROUND = COL_LOWLIGHT, COL_BLACK, COL_WHITE, COL_ERROR, COL_GRID, COL_FLASH, COL_DRAGON, COL_DRAGOFF, NCOLOURS }; /* Macro ickery copied from slant.c */ #define DIFFLIST(A) \ A(EASY,Easy,e) \ A(TRICKY,Tricky,t) #define ENUM(upper,title,lower) DIFF_ ## upper, #define TITLE(upper,title,lower) #title, #define ENCODE(upper,title,lower) #lower #define CONFIG(upper,title,lower) ":" #title enum { DIFFLIST(ENUM) DIFFCOUNT }; static char const *const pearl_diffnames[] = { DIFFLIST(TITLE) "(count)" }; static char const pearl_diffchars[] = DIFFLIST(ENCODE); #define DIFFCONFIG DIFFLIST(CONFIG) struct game_params { int w, h; int difficulty; bool nosolve; /* XXX remove me! */ }; struct shared_state { int w, h, sz; char *clues; /* size w*h */ int refcnt; }; #define INGRID(state, gx, gy) ((gx) >= 0 && (gx) < (state)->shared->w && \ (gy) >= 0 && (gy) < (state)->shared->h) struct game_state { struct shared_state *shared; char *lines; /* size w*h: lines placed */ char *errors; /* size w*h: errors detected */ char *marks; /* size w*h: 'no line here' marks placed. */ bool completed, used_solve; }; #define DEFAULT_PRESET 3 static const struct game_params pearl_presets[] = { {6, 6, DIFF_EASY}, {6, 6, DIFF_TRICKY}, {8, 8, DIFF_EASY}, {8, 8, DIFF_TRICKY}, {10, 10, DIFF_EASY}, {10, 10, DIFF_TRICKY}, {12, 8, DIFF_EASY}, {12, 8, DIFF_TRICKY}, }; static game_params *default_params(void) { game_params *ret = snew(game_params); *ret = pearl_presets[DEFAULT_PRESET]; ret->nosolve = false; return ret; } static bool game_fetch_preset(int i, char **name, game_params **params) { game_params *ret; char buf[64]; if (i < 0 || i >= lenof(pearl_presets)) return false; ret = default_params(); *ret = pearl_presets[i]; /* struct copy */ *params = ret; sprintf(buf, "%dx%d %s", pearl_presets[i].w, pearl_presets[i].h, pearl_diffnames[pearl_presets[i].difficulty]); *name = dupstr(buf); return true; } static void free_params(game_params *params) { sfree(params); } static game_params *dup_params(const game_params *params) { game_params *ret = snew(game_params); *ret = *params; /* structure copy */ return ret; } static void decode_params(game_params *ret, char const *string) { ret->w = ret->h = atoi(string); while (*string && isdigit((unsigned char) *string)) ++string; if (*string == 'x') { string++; ret->h = atoi(string); while (*string && isdigit((unsigned char)*string)) string++; } ret->difficulty = DIFF_EASY; if (*string == 'd') { int i; string++; for (i = 0; i < DIFFCOUNT; i++) if (*string == pearl_diffchars[i]) ret->difficulty = i; if (*string) string++; } ret->nosolve = false; if (*string == 'n') { ret->nosolve = true; string++; } } static char *encode_params(const game_params *params, bool full) { char buf[256]; sprintf(buf, "%dx%d", params->w, params->h); if (full) sprintf(buf + strlen(buf), "d%c%s", pearl_diffchars[params->difficulty], params->nosolve ? "n" : ""); return dupstr(buf); } static config_item *game_configure(const game_params *params) { config_item *ret; char buf[64]; ret = snewn(5, config_item); ret[0].name = "Width"; ret[0].type = C_STRING; sprintf(buf, "%d", params->w); ret[0].u.string.sval = dupstr(buf); ret[1].name = "Height"; ret[1].type = C_STRING; sprintf(buf, "%d", params->h); ret[1].u.string.sval = dupstr(buf); ret[2].name = "Difficulty"; ret[2].type = C_CHOICES; ret[2].u.choices.choicenames = DIFFCONFIG; ret[2].u.choices.selected = params->difficulty; ret[3].name = "Allow unsoluble"; ret[3].type = C_BOOLEAN; ret[3].u.boolean.bval = params->nosolve; ret[4].name = NULL; ret[4].type = C_END; return ret; } static game_params *custom_params(const config_item *cfg) { game_params *ret = snew(game_params); ret->w = atoi(cfg[0].u.string.sval); ret->h = atoi(cfg[1].u.string.sval); ret->difficulty = cfg[2].u.choices.selected; ret->nosolve = cfg[3].u.boolean.bval; return ret; } static const char *validate_params(const game_params *params, bool full) { if (params->w < 5) return "Width must be at least five"; if (params->h < 5) return "Height must be at least five"; if (params->w > INT_MAX / params->h) return "Width times height must not be unreasonably large"; if (params->difficulty < 0 || params->difficulty >= DIFFCOUNT) return "Unknown difficulty level"; if (params->difficulty >= DIFF_TRICKY && params->w + params->h < 11) return "Width or height must be at least six for Tricky"; return NULL; } /* ---------------------------------------------------------------------- * Solver. */ static int pearl_solve(int w, int h, char *clues, char *result, int difficulty, bool partial) { int W = 2*w+1, H = 2*h+1; short *workspace; DSF *dsf; int *dsfsize; int x, y, b, d; int ret = -1; /* * workspace[(2*y+1)*W+(2*x+1)] indicates the possible nature * of the square (x,y), as a logical OR of bitfields. * * workspace[(2*y)*W+(2*x+1)], for x odd and y even, indicates * whether the horizontal edge between (x,y) and (x+1,y) is * connected (1), disconnected (2) or unknown (3). * * workspace[(2*y+1)*W+(2*x)], indicates the same about the * vertical edge between (x,y) and (x,y+1). * * Initially, every square is considered capable of being in * any of the seven possible states (two straights, four * corners and empty), except those corresponding to clue * squares which are more restricted. * * Initially, all edges are unknown, except the ones around the * grid border which are known to be disconnected. */ workspace = snewn(W*H, short); for (x = 0; x < W*H; x++) workspace[x] = 0; /* Square states */ for (y = 0; y < h; y++) for (x = 0; x < w; x++) switch (clues[y*w+x]) { case CORNER: workspace[(2*y+1)*W+(2*x+1)] = bLU|bLD|bRU|bRD; break; case STRAIGHT: workspace[(2*y+1)*W+(2*x+1)] = bLR|bUD; break; default: workspace[(2*y+1)*W+(2*x+1)] = bLR|bUD|bLU|bLD|bRU|bRD|bBLANK; break; } /* Horizontal edges */ for (y = 0; y <= h; y++) for (x = 0; x < w; x++) workspace[(2*y)*W+(2*x+1)] = (y==0 || y==h ? 2 : 3); /* Vertical edges */ for (y = 0; y < h; y++) for (x = 0; x <= w; x++) workspace[(2*y+1)*W+(2*x)] = (x==0 || x==w ? 2 : 3); /* * We maintain a dsf of connected squares, together with a * count of the size of each equivalence class. */ dsf = dsf_new(w*h); dsfsize = snewn(w*h, int); /* * Now repeatedly try to find something we can do. */ while (1) { bool done_something = false; #ifdef SOLVER_DIAGNOSTICS for (y = 0; y < H; y++) { for (x = 0; x < W; x++) printf("%*x", (x&1) ? 5 : 2, workspace[y*W+x]); printf("\n"); } #endif /* * Go through the square state words, and discard any * square state which is inconsistent with known facts * about the edges around the square. */ for (y = 0; y < h; y++) for (x = 0; x < w; x++) { for (b = 0; b < 0xD; b++) if (workspace[(2*y+1)*W+(2*x+1)] & (1<<b)) { /* * If any edge of this square is known to * be connected when state b would require * it disconnected, or vice versa, discard * the state. */ for (d = 1; d <= 8; d += d) { int ex = 2*x+1 + DX(d), ey = 2*y+1 + DY(d); if (workspace[ey*W+ex] == ((b & d) ? 2 : 1)) { workspace[(2*y+1)*W+(2*x+1)] &= ~(1<<b); #ifdef SOLVER_DIAGNOSTICS printf("edge (%d,%d)-(%d,%d) rules out state" " %d for square (%d,%d)\n", ex/2, ey/2, (ex+1)/2, (ey+1)/2, b, x, y); #endif done_something = true; break; } } } /* * Consistency check: each square must have at * least one state left! */ if (!workspace[(2*y+1)*W+(2*x+1)]) { #ifdef SOLVER_DIAGNOSTICS printf("edge check at (%d,%d): inconsistency\n", x, y); #endif ret = 0; goto cleanup; } } /* * Now go through the states array again, and nail down any * unknown edge if one of its neighbouring squares makes it * known. */ for (y = 0; y < h; y++) for (x = 0; x < w; x++) { int edgeor = 0, edgeand = 15; for (b = 0; b < 0xD; b++) if (workspace[(2*y+1)*W+(2*x+1)] & (1<<b)) { edgeor |= b; edgeand &= b; } /* * Now any bit clear in edgeor marks a disconnected * edge, and any bit set in edgeand marks a * connected edge. */ /* First check consistency: neither bit is both! */ if (edgeand & ~edgeor) { #ifdef SOLVER_DIAGNOSTICS printf("square check at (%d,%d): inconsistency\n", x, y); #endif ret = 0; goto cleanup; } for (d = 1; d <= 8; d += d) { int ex = 2*x+1 + DX(d), ey = 2*y+1 + DY(d); if (!(edgeor & d) && workspace[ey*W+ex] == 3) { workspace[ey*W+ex] = 2; done_something = true; #ifdef SOLVER_DIAGNOSTICS printf("possible states of square (%d,%d) force edge" " (%d,%d)-(%d,%d) to be disconnected\n", x, y, ex/2, ey/2, (ex+1)/2, (ey+1)/2); #endif } else if ((edgeand & d) && workspace[ey*W+ex] == 3) { workspace[ey*W+ex] = 1; done_something = true; #ifdef SOLVER_DIAGNOSTICS printf("possible states of square (%d,%d) force edge" " (%d,%d)-(%d,%d) to be connected\n", x, y, ex/2, ey/2, (ex+1)/2, (ey+1)/2); #endif } } } if (done_something) continue; /* * Now for longer-range clue-based deductions (using the * rules that a corner clue must connect to two straight * squares, and a straight clue must connect to at least * one corner square). */ for (y = 0; y < h; y++) for (x = 0; x < w; x++) switch (clues[y*w+x]) { case CORNER: for (d = 1; d <= 8; d += d) { int ex = 2*x+1 + DX(d), ey = 2*y+1 + DY(d); int fx = ex + DX(d), fy = ey + DY(d); int type = d | F(d); if (workspace[ey*W+ex] == 1) { /* * If a corner clue is connected on any * edge, then we can immediately nail * down the square beyond that edge as * being a straight in the appropriate * direction. */ if (workspace[fy*W+fx] != (1<<type)) { workspace[fy*W+fx] = (1<<type); done_something = true; #ifdef SOLVER_DIAGNOSTICS printf("corner clue at (%d,%d) forces square " "(%d,%d) into state %d\n", x, y, fx/2, fy/2, type); #endif } } else if (workspace[ey*W+ex] == 3) { /* * Conversely, if a corner clue is * separated by an unknown edge from a * square which _cannot_ be a straight * in the appropriate direction, we can * mark that edge as disconnected. */ if (!(workspace[fy*W+fx] & (1<<type))) { workspace[ey*W+ex] = 2; done_something = true; #ifdef SOLVER_DIAGNOSTICS printf("corner clue at (%d,%d), plus square " "(%d,%d) not being state %d, " "disconnects edge (%d,%d)-(%d,%d)\n", x, y, fx/2, fy/2, type, ex/2, ey/2, (ex+1)/2, (ey+1)/2); #endif } } } break; case STRAIGHT: /* * If a straight clue is between two squares * neither of which is capable of being a * corner connected to it, then the straight * clue cannot point in that direction. */ for (d = 1; d <= 2; d += d) { int fx = 2*x+1 + 2*DX(d), fy = 2*y+1 + 2*DY(d); int gx = 2*x+1 - 2*DX(d), gy = 2*y+1 - 2*DY(d); int type = d | F(d); if (!(workspace[(2*y+1)*W+(2*x+1)] & (1<<type))) continue; if (!(workspace[fy*W+fx] & ((1<<(F(d)|A(d))) | (1<<(F(d)|C(d))))) && !(workspace[gy*W+gx] & ((1<<( d |A(d))) | (1<<( d |C(d)))))) { workspace[(2*y+1)*W+(2*x+1)] &= ~(1<<type); done_something = true; #ifdef SOLVER_DIAGNOSTICS printf("straight clue at (%d,%d) cannot corner at " "(%d,%d) or (%d,%d) so is not state %d\n", x, y, fx/2, fy/2, gx/2, gy/2, type); #endif } } /* * If a straight clue with known direction is * connected on one side to a known straight, * then on the other side it must be a corner. */ for (d = 1; d <= 8; d += d) { int fx = 2*x+1 + 2*DX(d), fy = 2*y+1 + 2*DY(d); int gx = 2*x+1 - 2*DX(d), gy = 2*y+1 - 2*DY(d); int type = d | F(d); if (workspace[(2*y+1)*W+(2*x+1)] != (1<<type)) continue; if (!(workspace[fy*W+fx] &~ (bLR|bUD)) && (workspace[gy*W+gx] &~ (bLU|bLD|bRU|bRD))) { workspace[gy*W+gx] &= (bLU|bLD|bRU|bRD); done_something = true; #ifdef SOLVER_DIAGNOSTICS printf("straight clue at (%d,%d) connecting to " "straight at (%d,%d) makes (%d,%d) a " "corner\n", x, y, fx/2, fy/2, gx/2, gy/2); #endif } } break; } if (done_something) continue; /* * Now detect shortcut loops. */ { int nonblanks, loopclass; dsf_reinit(dsf); for (x = 0; x < w*h; x++) dsfsize[x] = 1; /* * First go through the edge entries and update the dsf * of which squares are connected to which others. We * also track the number of squares in each equivalence * class, and count the overall number of * known-non-blank squares. * * In the process of doing this, we must notice if a * loop has already been formed. If it has, we blank * out any square which isn't part of that loop * (failing a consistency check if any such square does * not have BLANK as one of its remaining options) and * exit the deduction loop with success. */ nonblanks = 0; loopclass = -1; for (y = 1; y < H-1; y++) for (x = 1; x < W-1; x++) if ((y ^ x) & 1) { /* * (x,y) are the workspace coordinates of * an edge field. Compute the normal-space * coordinates of the squares it connects. */ int ax = (x-1)/2, ay = (y-1)/2, ac = ay*w+ax; int bx = x/2, by = y/2, bc = by*w+bx; /* * If the edge is connected, do the dsf * thing. */ if (workspace[y*W+x] == 1) { int ae, be; ae = dsf_canonify(dsf, ac); be = dsf_canonify(dsf, bc); if (ae == be) { /* * We have a loop! */ if (loopclass != -1) { /* * In fact, we have two * separate loops, which is * doom. */ #ifdef SOLVER_DIAGNOSTICS printf("two loops found in grid!\n"); #endif ret = 0; goto cleanup; } loopclass = ae; } else { /* * Merge the two equivalence * classes. */ int size = dsfsize[ae] + dsfsize[be]; dsf_merge(dsf, ac, bc); ae = dsf_canonify(dsf, ac); dsfsize[ae] = size; } } } else if ((y & x) & 1) { /* * (x,y) are the workspace coordinates of a * square field. If the square is * definitely not blank, count it. */ if (!(workspace[y*W+x] & bBLANK)) nonblanks++; } /* * If we discovered an existing loop above, we must now * blank every square not part of it, and exit the main * deduction loop. */ if (loopclass != -1) { #ifdef SOLVER_DIAGNOSTICS printf("loop found in grid!\n"); #endif for (y = 0; y < h; y++) for (x = 0; x < w; x++) if (dsf_canonify(dsf, y*w+x) != loopclass) { if (workspace[(y*2+1)*W+(x*2+1)] & bBLANK) { workspace[(y*2+1)*W+(x*2+1)] = bBLANK; } else { /* * This square is not part of the * loop, but is known non-blank. We * have goofed. */ #ifdef SOLVER_DIAGNOSTICS printf("non-blank square (%d,%d) found outside" " loop!\n", x, y); #endif ret = 0; goto cleanup; } } /* * And we're done. */ ret = 1; break; } /* Further deductions are considered 'tricky'. */ if (difficulty == DIFF_EASY) goto done_deductions; /* * Now go through the workspace again and mark any edge * which would cause a shortcut loop (i.e. would * connect together two squares in the same equivalence * class, and that equivalence class does not contain * _all_ the known-non-blank squares currently in the * grid) as disconnected. Also, mark any _square state_ * which would cause a shortcut loop as disconnected. */ for (y = 1; y < H-1; y++) for (x = 1; x < W-1; x++) if ((y ^ x) & 1) { /* * (x,y) are the workspace coordinates of * an edge field. Compute the normal-space * coordinates of the squares it connects. */ int ax = (x-1)/2, ay = (y-1)/2, ac = ay*w+ax; int bx = x/2, by = y/2, bc = by*w+bx; /* * If the edge is currently unknown, and * sits between two squares in the same * equivalence class, and the size of that * class is less than nonblanks, then * connecting this edge would be a shortcut * loop and so we must not do so. */ if (workspace[y*W+x] == 3) { int ae, be; ae = dsf_canonify(dsf, ac); be = dsf_canonify(dsf, bc); if (ae == be) { /* * We have a loop. Is it a shortcut? */ if (dsfsize[ae] < nonblanks) { /* * Yes! Mark this edge disconnected. */ workspace[y*W+x] = 2; done_something = true; #ifdef SOLVER_DIAGNOSTICS printf("edge (%d,%d)-(%d,%d) would create" " a shortcut loop, hence must be" " disconnected\n", x/2, y/2, (x+1)/2, (y+1)/2); #endif } } } } else if ((y & x) & 1) { /* * (x,y) are the workspace coordinates of a * square field. Go through its possible * (non-blank) states and see if any gives * rise to a shortcut loop. * * This is slightly fiddly, because we have * to check whether this square is already * part of the same equivalence class as * the things it's joining. */ int ae = dsf_canonify(dsf, (y/2)*w+(x/2)); for (b = 2; b < 0xD; b++) if (workspace[y*W+x] & (1<<b)) { /* * Find the equivalence classes of * the two squares this one would * connect if it were in this * state. */ int e = -1; for (d = 1; d <= 8; d += d) if (b & d) { int xx = x/2 + DX(d), yy = y/2 + DY(d); int ee = dsf_canonify(dsf, yy*w+xx); if (e == -1) ee = e; else if (e != ee) e = -2; } if (e >= 0) { /* * This square state would form * a loop on equivalence class * e. Measure the size of that * loop, and see if it's a * shortcut. */ int loopsize = dsfsize[e]; if (e != ae) loopsize++;/* add the square itself */ if (loopsize < nonblanks) { /* * It is! Mark this square * state invalid. */ workspace[y*W+x] &= ~(1<<b); done_something = true; #ifdef SOLVER_DIAGNOSTICS printf("square (%d,%d) would create a " "shortcut loop in state %d, " "hence cannot be\n", x/2, y/2, b); #endif } } } } } done_deductions: if (done_something) continue; /* * If we reach here, there is nothing left we can do. * Return 2 for ambiguous puzzle. */ ret = 2; break; } cleanup: /* * If ret = 1 then we've successfully achieved a solution. This * means that we expect every square to be nailed down to * exactly one possibility. If this is the case, or if the caller * asked for a partial solution anyway, transcribe those * possibilities into the result array. */ if (ret == 1 || partial) { for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { for (b = 0; b < 0xD; b++) if (workspace[(2*y+1)*W+(2*x+1)] == (1<<b)) { result[y*w+x] = b; break; } if (ret == 1) assert(b < 0xD); /* we should have had a break by now */ } } /* * Ensure we haven't left the _data structure_ inconsistent, * regardless of the consistency of the _puzzle_. In * particular, we should never have marked one square as * linked to its neighbour if the neighbour is not * reciprocally linked back to the original square. * * This can happen if we get part way through solving an * impossible puzzle and then give up trying to make further * progress. So here we fix it up to avoid confusing the rest * of the game. */ for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { for (d = 1; d <= 8; d += d) { int nx = x + DX(d), ny = y + DY(d); int rlink; if (0 <= nx && nx < w && 0 <= ny && ny < h) rlink = result[ny*w+nx] & F(d); else rlink = 0; /* off-board squares don't link back */ /* If other square doesn't link to us, don't link to it */ if (!rlink) result[y*w+x] &= ~d; } } } } sfree(dsfsize); dsf_free(dsf); sfree(workspace); assert(ret >= 0); return ret; } /* ---------------------------------------------------------------------- * Loop generator. */ /* * We use the loop generator code from loopy, hard-coding to a square * grid of the appropriate size. Knowing the grid layout and the tile * size we can shrink that to our small grid and then make our line * layout from the face colour info. * * We provide a bias function to the loop generator which tries to * bias in favour of loops with more scope for Pearl black clues. This * seems to improve the success rate of the puzzle generator, in that * such loops have a better chance of being soluble with all valid * clues put in. */ struct pearl_loopgen_bias_ctx { /* * Our bias function counts the number of 'black clue' corners * (i.e. corners adjacent to two straights) in both the * BLACK/nonBLACK and WHITE/nonWHITE boundaries. In order to do * this, we must: * * - track the edges that are part of each of those loops * - track the types of vertex in each loop (corner, straight, * none) * - track the current black-clue status of each vertex in each * loop. * * Each of these chunks of data is updated incrementally from the * previous one, to avoid slowdown due to the bias function * rescanning the whole grid every time it's called. * * So we need a lot of separate arrays, plus a tdq for each one, * and we must repeat it all twice for the BLACK and WHITE * boundaries. */ struct pearl_loopgen_bias_ctx_boundary { int colour; /* FACE_WHITE or FACE_BLACK */ bool *edges; /* is each edge part of the loop? */ tdq *edges_todo; char *vertextypes; /* bits 0-3 == outgoing edge bitmap; * bit 4 set iff corner clue. * Hence, 0 means non-vertex; * nonzero but bit 4 zero = straight. */ int *neighbour[2]; /* indices of neighbour vertices in loop */ tdq *vertextypes_todo; char *blackclues; /* is each vertex a black clue site? */ tdq *blackclues_todo; } boundaries[2]; /* boundaries[0]=WHITE, [1]=BLACK */ char *faces; /* remember last-seen colour of each face */ tdq *faces_todo; int score; grid *g; }; static int pearl_loopgen_bias(void *vctx, char *board, int face) { struct pearl_loopgen_bias_ctx *ctx = (struct pearl_loopgen_bias_ctx *)vctx; grid *g = ctx->g; int oldface, newface; int i, j, k; tdq_add(ctx->faces_todo, face); while ((j = tdq_remove(ctx->faces_todo)) >= 0) { oldface = ctx->faces[j]; ctx->faces[j] = newface = board[j]; for (i = 0; i < 2; i++) { struct pearl_loopgen_bias_ctx_boundary *b = &ctx->boundaries[i]; int c = b->colour; /* * If the face has changed either from or to colour c, we need * to reprocess the edges for this boundary. */ if (oldface == c || newface == c) { grid_face *f = &g->faces[face]; for (k = 0; k < f->order; k++) tdq_add(b->edges_todo, f->edges[k] - g->edges); } } } for (i = 0; i < 2; i++) { struct pearl_loopgen_bias_ctx_boundary *b = &ctx->boundaries[i]; int c = b->colour; /* * Go through the to-do list of edges. For each edge, decide * anew whether it's part of this boundary or not. Any edge * that changes state has to have both its endpoints put on * the vertextypes_todo list. */ while ((j = tdq_remove(b->edges_todo)) >= 0) { grid_edge *e = &g->edges[j]; int fc1 = e->face1 ? board[e->face1 - g->faces] : FACE_BLACK; int fc2 = e->face2 ? board[e->face2 - g->faces] : FACE_BLACK; bool oldedge = b->edges[j]; bool newedge = (fc1==c) ^ (fc2==c); if (oldedge != newedge) { b->edges[j] = newedge; tdq_add(b->vertextypes_todo, e->dot1 - g->dots); tdq_add(b->vertextypes_todo, e->dot2 - g->dots); } } /* * Go through the to-do list of vertices whose types need * refreshing. For each one, decide whether it's a corner, a * straight, or a vertex not in the loop, and in the former * two cases also work out the indices of its neighbour * vertices along the loop. Any vertex that changes state must * be put back on the to-do list for deciding if it's a black * clue site, and so must its two new neighbours _and_ its two * old neighbours. */ while ((j = tdq_remove(b->vertextypes_todo)) >= 0) { grid_dot *d = &g->dots[j]; int neighbours[2], type = 0, n = 0; for (k = 0; k < d->order; k++) { grid_edge *e = d->edges[k]; grid_dot *d2 = (e->dot1 == d ? e->dot2 : e->dot1); /* dir == 0,1,2,3 for an edge going L,U,R,D */ int dir = (d->y == d2->y) + 2*(d->x+d->y > d2->x+d2->y); int ei = e - g->edges; if (b->edges[ei]) { type |= 1 << dir; neighbours[n] = d2 - g->dots; n++; } } /* * Decide if it's a corner, and set the corner flag if so. */ if (type != 0 && type != 0x5 && type != 0xA) type |= 0x10; if (type != b->vertextypes[j]) { /* * Recompute old neighbours, if any. */ if (b->vertextypes[j]) { tdq_add(b->blackclues_todo, b->neighbour[0][j]); tdq_add(b->blackclues_todo, b->neighbour[1][j]); } /* * Recompute this vertex. */ tdq_add(b->blackclues_todo, j); b->vertextypes[j] = type; /* * Recompute new neighbours, if any. */ if (b->vertextypes[j]) { b->neighbour[0][j] = neighbours[0]; b->neighbour[1][j] = neighbours[1]; tdq_add(b->blackclues_todo, b->neighbour[0][j]); tdq_add(b->blackclues_todo, b->neighbour[1][j]); } } } /* * Go through the list of vertices which we must check to see * if they're black clue sites. Each one is a black clue site * iff it is a corner and its loop neighbours are non-corners. * Adjust the running total of black clues we've counted. */ while ((j = tdq_remove(b->blackclues_todo)) >= 0) { ctx->score -= b->blackclues[j]; b->blackclues[j] = ((b->vertextypes[j] & 0x10) && !((b->vertextypes[b->neighbour[0][j]] | b->vertextypes[b->neighbour[1][j]]) & 0x10)); ctx->score += b->blackclues[j]; } } return ctx->score; } static void pearl_loopgen(int w, int h, char *lines, random_state *rs) { grid *g = grid_new(GRID_SQUARE, w-1, h-1, NULL); char *board = snewn(g->num_faces, char); int i, s = g->tilesize; struct pearl_loopgen_bias_ctx biasctx; memset(lines, 0, w*h); /* * Initialise the context for the bias function. Initially we fill * all the to-do lists, so that the first call will scan * everything; thereafter the lists stay empty so we make * incremental changes. */ biasctx.g = g; biasctx.faces = snewn(g->num_faces, char); biasctx.faces_todo = tdq_new(g->num_faces); tdq_fill(biasctx.faces_todo); biasctx.score = 0; memset(biasctx.faces, FACE_GREY, g->num_faces); for (i = 0; i < 2; i++) { biasctx.boundaries[i].edges = snewn(g->num_edges, bool); memset(biasctx.boundaries[i].edges, 0, g->num_edges * sizeof(bool)); biasctx.boundaries[i].edges_todo = tdq_new(g->num_edges); tdq_fill(biasctx.boundaries[i].edges_todo); biasctx.boundaries[i].vertextypes = snewn(g->num_dots, char); memset(biasctx.boundaries[i].vertextypes, 0, g->num_dots); biasctx.boundaries[i].neighbour[0] = snewn(g->num_dots, int); biasctx.boundaries[i].neighbour[1] = snewn(g->num_dots, int); biasctx.boundaries[i].vertextypes_todo = tdq_new(g->num_dots); tdq_fill(biasctx.boundaries[i].vertextypes_todo); biasctx.boundaries[i].blackclues = snewn(g->num_dots, char); memset(biasctx.boundaries[i].blackclues, 0, g->num_dots); biasctx.boundaries[i].blackclues_todo = tdq_new(g->num_dots); tdq_fill(biasctx.boundaries[i].blackclues_todo); } biasctx.boundaries[0].colour = FACE_WHITE; biasctx.boundaries[1].colour = FACE_BLACK; generate_loop(g, board, rs, pearl_loopgen_bias, &biasctx); sfree(biasctx.faces); tdq_free(biasctx.faces_todo); for (i = 0; i < 2; i++) { sfree(biasctx.boundaries[i].edges); tdq_free(biasctx.boundaries[i].edges_todo); sfree(biasctx.boundaries[i].vertextypes); sfree(biasctx.boundaries[i].neighbour[0]); sfree(biasctx.boundaries[i].neighbour[1]); tdq_free(biasctx.boundaries[i].vertextypes_todo); sfree(biasctx.boundaries[i].blackclues); tdq_free(biasctx.boundaries[i].blackclues_todo); } for (i = 0; i < g->num_edges; i++) { grid_edge *e = g->edges + i; enum face_colour c1 = FACE_COLOUR(e->face1); enum face_colour c2 = FACE_COLOUR(e->face2); assert(c1 != FACE_GREY); assert(c2 != FACE_GREY); if (c1 != c2) { /* This grid edge is on the loop: lay line along it */ int x1 = e->dot1->x/s, y1 = e->dot1->y/s; int x2 = e->dot2->x/s, y2 = e->dot2->y/s; /* (x1,y1) and (x2,y2) are now in our grid coords (0-w,0-h). */ if (x1 == x2) { if (y1 > y2) SWAP(y1,y2); assert(y1+1 == y2); lines[y1*w+x1] |= D; lines[y2*w+x1] |= U; } else if (y1 == y2) { if (x1 > x2) SWAP(x1,x2); assert(x1+1 == x2); lines[y1*w+x1] |= R; lines[y1*w+x2] |= L; } else assert(!"grid with diagonal coords?!"); } } grid_free(g); sfree(board); #if defined LOOPGEN_DIAGNOSTICS && !defined GENERATION_DIAGNOSTICS printf("as returned:\n"); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { int type = lines[y*w+x]; char s[5], *p = s; if (type & L) *p++ = 'L'; if (type & R) *p++ = 'R'; if (type & U) *p++ = 'U'; if (type & D) *p++ = 'D'; *p = '\0'; printf("%3s", s); } printf("\n"); } printf("\n"); #endif } static int new_clues(const game_params *params, random_state *rs, char *clues, char *grid) { int w = params->w, h = params->h, diff = params->difficulty; int ngen = 0, x, y, d, ret, i; /* * Difficulty exception: 5x5 Tricky is not generable (the * generator will spin forever trying) and so we fudge it to Easy. */ if (w == 5 && h == 5 && diff > DIFF_EASY) diff = DIFF_EASY; while (1) { ngen++; pearl_loopgen(w, h, grid, rs); #ifdef GENERATION_DIAGNOSTICS printf("grid array:\n"); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { int type = grid[y*w+x]; char s[5], *p = s; if (type & L) *p++ = 'L'; if (type & R) *p++ = 'R'; if (type & U) *p++ = 'U'; if (type & D) *p++ = 'D'; *p = '\0'; printf("%2s ", s); } printf("\n"); } printf("\n"); #endif /* * Set up the maximal clue array. */ for (y = 0; y < h; y++) for (x = 0; x < w; x++) { int type = grid[y*w+x]; clues[y*w+x] = NOCLUE; if ((bLR|bUD) & (1 << type)) { /* * This is a straight; see if it's a viable * candidate for a straight clue. It qualifies if * at least one of the squares it connects to is a * corner. */ for (d = 1; d <= 8; d += d) if (type & d) { int xx = x + DX(d), yy = y + DY(d); assert(xx >= 0 && xx < w && yy >= 0 && yy < h); if ((bLU|bLD|bRU|bRD) & (1 << grid[yy*w+xx])) break; } if (d <= 8) /* we found one */ clues[y*w+x] = STRAIGHT; } else if ((bLU|bLD|bRU|bRD) & (1 << type)) { /* * This is a corner; see if it's a viable candidate * for a corner clue. It qualifies if all the * squares it connects to are straights. */ for (d = 1; d <= 8; d += d) if (type & d) { int xx = x + DX(d), yy = y + DY(d); assert(xx >= 0 && xx < w && yy >= 0 && yy < h); if (!((bLR|bUD) & (1 << grid[yy*w+xx]))) break; } if (d > 8) /* we didn't find a counterexample */ clues[y*w+x] = CORNER; } } #ifdef GENERATION_DIAGNOSTICS printf("clue array:\n"); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { printf("%c", " *O"[(unsigned char)clues[y*w+x]]); } printf("\n"); } printf("\n"); #endif if (!params->nosolve) { int *cluespace, *straights, *corners; int nstraights, ncorners, nstraightpos, ncornerpos; /* * See if we can solve the puzzle just like this. */ ret = pearl_solve(w, h, clues, grid, diff, false); assert(ret > 0); /* shouldn't be inconsistent! */ if (ret != 1) continue; /* go round and try again */ /* * Check this puzzle isn't too easy. */ if (diff > DIFF_EASY) { ret = pearl_solve(w, h, clues, grid, diff-1, false); assert(ret > 0); if (ret == 1) continue; /* too easy: try again */ } /* * Now shuffle the grid points and gradually remove the * clues to find a minimal set which still leaves the * puzzle soluble. * * We preferentially attempt to remove whichever type of * clue is currently most numerous, to combat a general * tendency of plain random generation to bias in favour * of many white clues and few black. * * 'nstraights' and 'ncorners' count the number of clues * of each type currently remaining in the grid; * 'nstraightpos' and 'ncornerpos' count the clues of each * type we have left to try to remove. (Clues which we * have tried and failed to remove are counted by the * former but not the latter.) */ cluespace = snewn(w*h, int); straights = cluespace; nstraightpos = 0; for (i = 0; i < w*h; i++) if (clues[i] == STRAIGHT) straights[nstraightpos++] = i; corners = straights + nstraightpos; ncornerpos = 0; for (i = 0; i < w*h; i++) if (clues[i] == STRAIGHT) corners[ncornerpos++] = i; nstraights = nstraightpos; ncorners = ncornerpos; shuffle(straights, nstraightpos, sizeof(*straights), rs); shuffle(corners, ncornerpos, sizeof(*corners), rs); while (nstraightpos > 0 || ncornerpos > 0) { int cluepos; int clue; /* * Decide which clue to try to remove next. If both * types are available, we choose whichever kind is * currently overrepresented; otherwise we take * whatever we can get. */ if (nstraightpos > 0 && ncornerpos > 0) { if (nstraights >= ncorners) cluepos = straights[--nstraightpos]; else cluepos = straights[--ncornerpos]; } else { if (nstraightpos > 0) cluepos = straights[--nstraightpos]; else cluepos = straights[--ncornerpos]; } y = cluepos / w; x = cluepos % w; clue = clues[y*w+x]; clues[y*w+x] = 0; /* try removing this clue */ ret = pearl_solve(w, h, clues, grid, diff, false); assert(ret > 0); if (ret != 1) clues[y*w+x] = clue; /* oops, put it back again */ } sfree(cluespace); } #ifdef FINISHED_PUZZLE printf("clue array:\n"); for (y = 0; y < h; y++) { for (x = 0; x < w; x++) { printf("%c", " *O"[(unsigned char)clues[y*w+x]]); } printf("\n"); } printf("\n"); #endif break; /* got it */ } debug(("%d %dx%d loops before finished puzzle.\n", ngen, w, h)); return ngen; } static char *new_game_desc(const game_params *params, random_state *rs, char **aux, bool interactive) { char *grid, *clues; char *desc; int w = params->w, h = params->h, i, j; grid = snewn(w*h, char); clues = snewn(w*h, char); new_clues(params, rs, clues, grid); desc = snewn(w * h + 1, char); for (i = j = 0; i < w*h; i++) { if (clues[i] == NOCLUE && j > 0 && desc[j-1] >= 'a' && desc[j-1] < 'z') desc[j-1]++; else if (clues[i] == NOCLUE) desc[j++] = 'a'; else if (clues[i] == CORNER) desc[j++] = 'B'; else if (clues[i] == STRAIGHT) desc[j++] = 'W'; } desc[j] = '\0'; *aux = snewn(w*h+1, char); for (i = 0; i < w*h; i++) (*aux)[i] = (grid[i] < 10) ? (grid[i] + '0') : (grid[i] + 'A' - 10); (*aux)[w*h] = '\0'; sfree(grid); sfree(clues); return desc; } static const char *validate_desc(const game_params *params, const char *desc) { int i, sizesofar; const int totalsize = params->w * params->h; sizesofar = 0; for (i = 0; desc[i]; i++) { if (desc[i] >= 'a' && desc[i] <= 'z') sizesofar += desc[i] - 'a' + 1; else if (desc[i] == 'B' || desc[i] == 'W') sizesofar++; else return "unrecognised character in string"; } if (sizesofar > totalsize) return "string too long"; else if (sizesofar < totalsize) return "string too short"; return NULL; } static game_state *new_game(midend *me, const game_params *params, const char *desc) { game_state *state = snew(game_state); int i, j, sz = params->w*params->h; state->completed = false; state->used_solve = false; state->shared = snew(struct shared_state); state->shared->w = params->w; state->shared->h = params->h; state->shared->sz = sz; state->shared->refcnt = 1; state->shared->clues = snewn(sz, char); for (i = j = 0; desc[i]; i++) { assert(j < sz); if (desc[i] >= 'a' && desc[i] <= 'z') { int n = desc[i] - 'a' + 1; assert(j + n <= sz); while (n-- > 0) state->shared->clues[j++] = NOCLUE; } else if (desc[i] == 'B') { state->shared->clues[j++] = CORNER; } else if (desc[i] == 'W') { state->shared->clues[j++] = STRAIGHT; } } state->lines = snewn(sz, char); state->errors = snewn(sz, char); state->marks = snewn(sz, char); for (i = 0; i < sz; i++) state->lines[i] = state->errors[i] = state->marks[i] = BLANK; return state; } static game_state *dup_game(const game_state *state) { game_state *ret = snew(game_state); int sz = state->shared->sz, i; ret->shared = state->shared; ret->completed = state->completed; ret->used_solve = state->used_solve; ++ret->shared->refcnt; ret->lines = snewn(sz, char); ret->errors = snewn(sz, char); ret->marks = snewn(sz, char); for (i = 0; i < sz; i++) { ret->lines[i] = state->lines[i]; ret->errors[i] = state->errors[i]; ret->marks[i] = state->marks[i]; } return ret; } static void free_game(game_state *state) { assert(state); if (--state->shared->refcnt == 0) { sfree(state->shared->clues); sfree(state->shared); } sfree(state->lines); sfree(state->errors); sfree(state->marks); sfree(state); } static char nbits[16] = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 }; #define NBITS(l) ( ((l) < 0 || (l) > 15) ? 4 : nbits[l] ) #define ERROR_CLUE 16 /* Returns false if the state is invalid. */ static bool dsf_update_completion(game_state *state, int ax, int ay, char dir, DSF *dsf) { int w = state->shared->w /*, h = state->shared->h */; int ac = ay*w+ax, bx, by, bc; if (!(state->lines[ac] & dir)) return true; /* no link */ bx = ax + DX(dir); by = ay + DY(dir); if (!INGRID(state, bx, by)) return false; /* should not have a link off grid */ bc = by*w+bx; if (!(state->lines[bc] & F(dir))) return false; /* should have reciprocal link */ if (!(state->lines[bc] & F(dir))) return true; dsf_merge(dsf, ac, bc); return true; } /* Returns false if the state is invalid. */ static bool check_completion(game_state *state, bool mark) { int w = state->shared->w, h = state->shared->h, x, y, i, d; bool had_error = false; DSF *dsf; int *component_state; int nsilly, nloop, npath, largest_comp, largest_size, total_pathsize; enum { COMP_NONE, COMP_LOOP, COMP_PATH, COMP_SILLY, COMP_EMPTY }; if (mark) { for (i = 0; i < w*h; i++) { state->errors[i] = 0; } } #define ERROR(x,y,e) do { had_error = true; if (mark) state->errors[(y)*w+(x)] |= (e); } while(0) /* * Analyse the solution into loops, paths and stranger things. * Basic strategy here is all the same as in Loopy - see the big * comment in loopy.c's check_completion() - and for exactly the * same reasons, since Loopy and Pearl have basically the same * form of expected solution. */ dsf = dsf_new(w*h); /* Build the dsf. */ for (x = 0; x < w; x++) { for (y = 0; y < h; y++) { if (!dsf_update_completion(state, x, y, R, dsf) || !dsf_update_completion(state, x, y, D, dsf)) { dsf_free(dsf); return false; } } } /* Initialise a state variable for each connected component. */ component_state = snewn(w*h, int); for (i = 0; i < w*h; i++) { if (dsf_canonify(dsf, i) == i) component_state[i] = COMP_LOOP; else component_state[i] = COMP_NONE; } /* * Classify components, and mark errors where a square has more * than two line segments. */ for (x = 0; x < w; x++) { for (y = 0; y < h; y++) { int type = state->lines[y*w+x]; int degree = NBITS(type); int comp = dsf_canonify(dsf, y*w+x); if (degree > 2) { ERROR(x,y,type); component_state[comp] = COMP_SILLY; } else if (degree == 0) { component_state[comp] = COMP_EMPTY; } else if (degree == 1) { if (component_state[comp] != COMP_SILLY) component_state[comp] = COMP_PATH; } } } /* Count the components, and find the largest sensible one. */ nsilly = nloop = npath = 0; total_pathsize = 0; largest_comp = largest_size = -1; for (i = 0; i < w*h; i++) { if (component_state[i] == COMP_SILLY) { nsilly++; } else if (component_state[i] == COMP_PATH) { total_pathsize += dsf_size(dsf, i); npath = 1; } else if (component_state[i] == COMP_LOOP) { int this_size; nloop++; if ((this_size = dsf_size(dsf, i)) > largest_size) { largest_comp = i; largest_size = this_size; } } } if (largest_size < total_pathsize) { largest_comp = -1; /* means the paths */ largest_size = total_pathsize; } if (nloop > 0 && nloop + npath > 1) { /* * If there are at least two sensible components including at * least one loop, highlight every sensible component that is * not the largest one. */ for (i = 0; i < w*h; i++) { int comp = dsf_canonify(dsf, i); if ((component_state[comp] == COMP_PATH && -1 != largest_comp) || (component_state[comp] == COMP_LOOP && comp != largest_comp)) ERROR(i%w, i/w, state->lines[i]); } } /* Now we've finished with the dsf and component states. The only * thing we'll need to remember later on is whether all edges were * part of a single loop, for which our counter variables * nsilly,nloop,npath are enough. */ sfree(component_state); dsf_free(dsf); /* * Check that no clues are contradicted. This code is similar to * the code that sets up the maximal clue array for any given * loop. */ for (x = 0; x < w; x++) { for (y = 0; y < h; y++) { int type = state->lines[y*w+x]; if (state->shared->clues[y*w+x] == CORNER) { /* Supposed to be a corner: will find a contradiction if * it actually contains a straight line, or if it touches any * corners. */ if ((bLR|bUD) & (1 << type)) { ERROR(x,y,ERROR_CLUE); /* actually straight */ } for (d = 1; d <= 8; d += d) if (type & d) { int xx = x + DX(d), yy = y + DY(d); if (!INGRID(state, xx, yy)) { ERROR(x,y,d); /* leads off grid */ } else { if ((bLU|bLD|bRU|bRD) & (1 << state->lines[yy*w+xx])) { ERROR(x,y,ERROR_CLUE); /* touches corner */ } } } } else if (state->shared->clues[y*w+x] == STRAIGHT) { /* Supposed to be straight: will find a contradiction if * it actually contains a corner, or if it only touches * straight lines. */ if ((bLU|bLD|bRU|bRD) & (1 << type)) { ERROR(x,y,ERROR_CLUE); /* actually a corner */ } i = 0; for (d = 1; d <= 8; d += d) if (type & d) { int xx = x + DX(d), yy = y + DY(d); if (!INGRID(state, xx, yy)) { ERROR(x,y,d); /* leads off grid */ } else { if ((bLR|bUD) & (1 << state->lines[yy*w+xx])) i++; /* a straight */ } } if (i >= 2 && NBITS(type) >= 2) { ERROR(x,y,ERROR_CLUE); /* everything touched is straight */ } } } } if (nloop == 1 && nsilly == 0 && npath == 0) { /* * If there's exactly one loop (so that the puzzle is at least * potentially complete), we need to ensure it hasn't left any * clue out completely. */ for (x = 0; x < w; x++) { for (y = 0; y < h; y++) { if (state->lines[y*w+x] == BLANK) { if (state->shared->clues[y*w+x] != NOCLUE) { /* the loop doesn't include this clue square! */ ERROR(x, y, ERROR_CLUE); } } } } /* * But if not, then we're done! */ if (!had_error) state->completed = true; } return true; } /* completion check: * * - no clues must be contradicted (highlight clue itself in error if so) * - if there is a closed loop it must include every line segment laid * - if there's a smaller closed loop then highlight whole loop as error * - no square must have more than 2 lines radiating from centre point * (highlight all lines in that square as error if so) */ static char *solve_for_diff(game_state *state, char *old_lines, char *new_lines) { int w = state->shared->w, h = state->shared->h, i; char *move = snewn(w*h*40, char), *p = move; *p++ = 'S'; for (i = 0; i < w*h; i++) { if (old_lines[i] != new_lines[i]) { p += sprintf(p, ";R%d,%d,%d", new_lines[i], i%w, i/w); } } *p++ = '\0'; move = sresize(move, p - move, char); return move; } static char *solve_game(const game_state *state, const game_state *currstate, const char *aux, const char **error) { game_state *solved = dup_game(state); int i, ret, sz = state->shared->sz; char *move; if (aux) { for (i = 0; i < sz; i++) { if (aux[i] >= '0' && aux[i] <= '9') solved->lines[i] = aux[i] - '0'; else if (aux[i] >= 'A' && aux[i] <= 'F') solved->lines[i] = aux[i] - 'A' + 10; else { *error = "invalid char in aux"; move = NULL; goto done; } } ret = 1; } else { /* Try to solve with present (half-solved) state first: if there's no * solution from there go back to original state. */ ret = pearl_solve(currstate->shared->w, currstate->shared->h, currstate->shared->clues, solved->lines, DIFFCOUNT, false); if (ret < 1) ret = pearl_solve(state->shared->w, state->shared->h, state->shared->clues, solved->lines, DIFFCOUNT, false); } if (ret < 1) { *error = "Unable to find solution"; move = NULL; } else { move = solve_for_diff(solved, currstate->lines, solved->lines); } done: free_game(solved); return move; } static bool game_can_format_as_text_now(const game_params *params) { return true; } static char *game_text_format(const game_state *state) { int w = state->shared->w, h = state->shared->h, cw = 4, ch = 2; int gw = cw*(w-1) + 2, gh = ch*(h-1) + 1, len = gw * gh, r, c, j; char *board = snewn(len + 1, char); assert(board); memset(board, ' ', len); for (r = 0; r < h; ++r) { for (c = 0; c < w; ++c) { int i = r*w + c, cell = r*ch*gw + c*cw; board[cell] = "+BW"[(unsigned char)state->shared->clues[i]]; if (c < w - 1 && (state->lines[i] & R || state->lines[i+1] & L)) memset(board + cell + 1, '-', cw - 1); if (r < h - 1 && (state->lines[i] & D || state->lines[i+w] & U)) for (j = 1; j < ch; ++j) board[cell + j*gw] = '|'; if (c < w - 1 && (state->marks[i] & R || state->marks[i+1] & L)) board[cell + cw/2] = 'x'; if (r < h - 1 && (state->marks[i] & D || state->marks[i+w] & U)) board[cell + (ch/2 * gw)] = 'x'; } for (j = 0; j < (r == h - 1 ? 1 : ch); ++j) board[r*ch*gw + (gw - 1) + j*gw] = '\n'; } board[len] = '\0'; return board; } struct game_ui { int *dragcoords; /* list of (y*w+x) coords in drag so far */ int ndragcoords; /* number of entries in dragcoords. * 0 = click but no drag yet. -1 = no drag at all */ int clickx, clicky; /* pixel position of initial click */ int curx, cury; /* grid position of keyboard cursor */ bool cursor_active; /* true iff cursor is shown */ /* * User preference: general visual style of the GUI. GUI_MASYU is * how this puzzle is traditionally presented, with clue dots in * the middle of grid squares, and the solution loop connecting * square-centres. GUI_LOOPY shifts the grid by half a square in * each direction, so that the clue dots are at _vertices_ of the * grid and the solution loop follows the grid edges, which you * could argue is more logical. */ enum { GUI_MASYU, GUI_LOOPY } gui_style; }; static void legacy_prefs_override(struct game_ui *ui_out) { static bool initialised = false; static int gui_style = -1; if (!initialised) { initialised = true; switch (getenv_bool("PEARL_GUI_LOOPY", -1)) { case 0: gui_style = GUI_MASYU; break; case 1: gui_style = GUI_LOOPY; break; } } if (gui_style != -1) ui_out->gui_style = gui_style; } static game_ui *new_ui(const game_state *state) { game_ui *ui = snew(game_ui); ui->ndragcoords = -1; ui->dragcoords = state ? snewn(state->shared->sz, int) : NULL; ui->cursor_active = getenv_bool("PUZZLES_SHOW_CURSOR", false); ui->curx = ui->cury = 0; ui->gui_style = GUI_MASYU; legacy_prefs_override(ui); return ui; } static void free_ui(game_ui *ui) { sfree(ui->dragcoords); sfree(ui); } static config_item *get_prefs(game_ui *ui) { config_item *ret; ret = snewn(2, config_item); ret[0].name = "Puzzle appearance"; ret[0].kw = "appearance"; ret[0].type = C_CHOICES; ret[0].u.choices.choicenames = ":Traditional:Loopy-style"; ret[0].u.choices.choicekws = ":traditional:loopy"; ret[0].u.choices.selected = ui->gui_style; ret[1].name = NULL; ret[1].type = C_END; return ret; } static void set_prefs(game_ui *ui, const config_item *cfg) { ui->gui_style = cfg[0].u.choices.selected; } static void game_changed_state(game_ui *ui, const game_state *oldstate, const game_state *newstate) { } static const char *current_key_label(const game_ui *ui, const game_state *state, int button) { if (IS_CURSOR_SELECT(button) && ui->cursor_active) { if (button == CURSOR_SELECT) { if (ui->ndragcoords == -1) return "Start"; return "Stop"; } if (button == CURSOR_SELECT2 && ui->ndragcoords >= 0) return "Cancel"; } return ""; } #define PREFERRED_TILE_SIZE 31 #define HALFSZ (ds->halfsz) #define TILE_SIZE (ds->halfsz*2 + 1) #define BORDER ((ui->gui_style == GUI_LOOPY) ? (TILE_SIZE/8) : (TILE_SIZE/2)) #define BORDER_WIDTH (max(TILE_SIZE / 32, 1)) #define COORD(x) ( (x) * TILE_SIZE + BORDER ) #define CENTERED_COORD(x) ( COORD(x) + TILE_SIZE/2 ) #define FROMCOORD(x) ( ((x) < BORDER) ? -1 : ( ((x) - BORDER) / TILE_SIZE) ) #define DS_ESHIFT 4 /* R/U/L/D shift, for error flags */ #define DS_DSHIFT 8 /* R/U/L/D shift, for drag-in-progress flags */ #define DS_MSHIFT 12 /* shift for no-line mark */ #define DS_ERROR_CLUE (1 << 20) #define DS_FLASH (1 << 21) #define DS_CURSOR (1 << 22) struct game_drawstate { int halfsz; bool started; int w, h, sz; unsigned int *lflags; /* size w*h */ char *draglines; /* size w*h; lines flipped by current drag */ }; /* * Routine shared between multiple callers to work out the intended * effect of a drag path on the grid. * * Call it in a loop, like this: * * bool clearing = true; * for (i = 0; i < ui->ndragcoords - 1; i++) { * int sx, sy, dx, dy, dir, oldstate, newstate; * interpret_ui_drag(state, ui, &clearing, i, &sx, &sy, &dx, &dy, * &dir, &oldstate, &newstate); * * [do whatever is needed to handle the fact that the drag * wants the edge from sx,sy to dx,dy (heading in direction * 'dir' at the sx,sy end) to be changed from state oldstate * to state newstate, each of which equals either 0 or dir] * } */ static void interpret_ui_drag(const game_state *state, const game_ui *ui, bool *clearing, int i, int *sx, int *sy, int *dx, int *dy, int *dir, int *oldstate, int *newstate) { int w = state->shared->w; int sp = ui->dragcoords[i], dp = ui->dragcoords[i+1]; *sy = sp/w; *sx = sp%w; *dy = dp/w; *dx = dp%w; *dir = (*dy>*sy ? D : *dy<*sy ? U : *dx>*sx ? R : L); *oldstate = state->lines[sp] & *dir; if (*oldstate) { /* * The edge we've dragged over was previously * present. Set it to absent, unless we've already * stopped doing that. */ *newstate = *clearing ? 0 : *dir; } else { /* * The edge we've dragged over was previously * absent. Set it to present, and cancel the * 'clearing' flag so that all subsequent edges in * the drag are set rather than cleared. */ *newstate = *dir; *clearing = false; } } static void update_ui_drag(const game_state *state, game_ui *ui, int gx, int gy) { int /* sz = state->shared->sz, */ w = state->shared->w; int i, ox, oy, pos; int lastpos; if (!INGRID(state, gx, gy)) return; /* square is outside grid */ if (ui->ndragcoords < 0) return; /* drag not in progress anyway */ pos = gy * w + gx; lastpos = ui->dragcoords[ui->ndragcoords > 0 ? ui->ndragcoords-1 : 0]; if (pos == lastpos) return; /* same square as last visited one */ /* Drag confirmed, if it wasn't already. */ if (ui->ndragcoords == 0) ui->ndragcoords = 1; /* * Dragging the mouse into a square that's already been visited by * the drag path so far has the effect of truncating the path back * to that square, so a player can back out part of an uncommitted * drag without having to let go of the mouse. * * An exception is that you're allowed to drag round in a loop * back to the very start of the drag, provided that doesn't * create a vertex of the wrong degree. This allows a player who's * after an extra challenge to draw the entire loop in a single * drag, without it cancelling itself just before release. */ for (i = 1; i < ui->ndragcoords; i++) if (pos == ui->dragcoords[i]) { ui->ndragcoords = i+1; return; } if (pos == ui->dragcoords[0]) { /* More complex check for a loop-shaped drag, which has to go * through interpret_ui_drag to decide on the final degree of * the start/end vertex. */ ui->dragcoords[ui->ndragcoords] = pos; bool clearing = true; int lines = state->lines[pos] & (L|R|U|D); for (i = 0; i < ui->ndragcoords; i++) { int sx, sy, dx, dy, dir, oldstate, newstate; interpret_ui_drag(state, ui, &clearing, i, &sx, &sy, &dx, &dy, &dir, &oldstate, &newstate); if (sx == gx && sy == gy) lines ^= (oldstate ^ newstate); if (dx == gx && dy == gy) lines ^= (F(oldstate) ^ F(newstate)); } if (NBITS(lines) > 2) { /* Bad vertex degree: fall back to the backtracking behaviour. */ ui->ndragcoords = 1; return; } } /* * Otherwise, dragging the mouse into a square that's a rook-move * away from the last one on the path extends the path. */ oy = ui->dragcoords[ui->ndragcoords-1] / w; ox = ui->dragcoords[ui->ndragcoords-1] % w; if (ox == gx || oy == gy) { int dx = (gx < ox ? -1 : gx > ox ? +1 : 0); int dy = (gy < oy ? -1 : gy > oy ? +1 : 0); int dir = (dy>0 ? D : dy<0 ? U : dx>0 ? R : L); while (ox != gx || oy != gy) { /* * If the drag attempts to cross a 'no line here' mark, * stop there. We physically don't allow the user to drag * over those marks. */ if (state->marks[oy*w+ox] & dir) break; ox += dx; oy += dy; ui->dragcoords[ui->ndragcoords++] = oy * w + ox; } } /* * Failing that, we do nothing at all: if the user has dragged * diagonally across the board, they'll just have to return the * mouse to the last known position and do whatever they meant to * do again, more slowly and clearly. */ } static char *mark_in_direction(const game_state *state, int x, int y, int dir, bool primary, char *buf) { int w = state->shared->w /*, h = state->shared->h, sz = state->shared->sz */; int x2 = x + DX(dir); int y2 = y + DY(dir); int dir2 = F(dir); char ch = primary ? 'F' : 'M', *other; if (!INGRID(state, x, y) || !INGRID(state, x2, y2)) return MOVE_UI_UPDATE; /* disallow laying a mark over a line, or vice versa. */ other = primary ? state->marks : state->lines; if (other[y*w+x] & dir || other[y2*w+x2] & dir2) return MOVE_UI_UPDATE; sprintf(buf, "%c%d,%d,%d;%c%d,%d,%d", ch, dir, x, y, ch, dir2, x2, y2); return dupstr(buf); } #define KEY_DIRECTION(btn) (\ (btn) == CURSOR_DOWN ? D : (btn) == CURSOR_UP ? U :\ (btn) == CURSOR_LEFT ? L : R) static char *interpret_move(const game_state *state, game_ui *ui, const game_drawstate *ds, int x, int y, int button) { int w = state->shared->w, h = state->shared->h /*, sz = state->shared->sz */; int gx = FROMCOORD(x), gy = FROMCOORD(y), i; bool release = false; char tmpbuf[80]; bool shift = button & MOD_SHFT, control = button & MOD_CTRL; button &= ~MOD_MASK; if (IS_MOUSE_DOWN(button)) { ui->cursor_active = false; if (!INGRID(state, gx, gy)) { ui->ndragcoords = -1; return MOVE_UNUSED; } ui->clickx = x; ui->clicky = y; ui->dragcoords[0] = gy * w + gx; ui->ndragcoords = 0; /* will be 1 once drag is confirmed */ return MOVE_UI_UPDATE; } if (button == LEFT_DRAG && ui->ndragcoords >= 0) { update_ui_drag(state, ui, gx, gy); return MOVE_UI_UPDATE; } if (IS_MOUSE_RELEASE(button)) release = true; if (IS_CURSOR_MOVE(button)) { if (!ui->cursor_active) { ui->cursor_active = true; } else if (control || shift) { char *move; if (ui->ndragcoords > 0) return MOVE_NO_EFFECT; ui->ndragcoords = -1; move = mark_in_direction(state, ui->curx, ui->cury, KEY_DIRECTION(button), control, tmpbuf); if (control && !shift && *move) move_cursor(button, &ui->curx, &ui->cury, w, h, false); return move; } else { move_cursor(button, &ui->curx, &ui->cury, w, h, false); if (ui->ndragcoords >= 0) update_ui_drag(state, ui, ui->curx, ui->cury); } return MOVE_UI_UPDATE; } if (IS_CURSOR_SELECT(button)) { if (!ui->cursor_active) { ui->cursor_active = true; return MOVE_UI_UPDATE; } else if (button == CURSOR_SELECT) { if (ui->ndragcoords == -1) { ui->ndragcoords = 0; ui->dragcoords[0] = ui->cury * w + ui->curx; ui->clickx = CENTERED_COORD(ui->curx); ui->clicky = CENTERED_COORD(ui->cury); return MOVE_UI_UPDATE; } else release = true; } else if (button == CURSOR_SELECT2) { if (ui->ndragcoords >= 0) { ui->ndragcoords = -1; return MOVE_UI_UPDATE; } return MOVE_NO_EFFECT; } } if (button == 27 || button == '\b') { if (ui->ndragcoords >= 0) { ui->ndragcoords = -1; return MOVE_UI_UPDATE; } return MOVE_NO_EFFECT; } if (release) { if (ui->ndragcoords > 0) { /* End of a drag: process the cached line data. */ int buflen = 0, bufsize = 256, tmplen; char *buf = NULL; const char *sep = ""; bool clearing = true; for (i = 0; i < ui->ndragcoords - 1; i++) { int sx, sy, dx, dy, dir, oldstate, newstate; interpret_ui_drag(state, ui, &clearing, i, &sx, &sy, &dx, &dy, &dir, &oldstate, &newstate); if (oldstate != newstate) { if (!buf) buf = snewn(bufsize, char); tmplen = sprintf(tmpbuf, "%sF%d,%d,%d;F%d,%d,%d", sep, dir, sx, sy, F(dir), dx, dy); if (buflen + tmplen >= bufsize) { bufsize = (buflen + tmplen) * 5 / 4 + 256; buf = sresize(buf, bufsize, char); } strcpy(buf + buflen, tmpbuf); buflen += tmplen; sep = ";"; } } ui->ndragcoords = -1; return buf ? buf : MOVE_UI_UPDATE; } else if (ui->ndragcoords == 0) { /* Click (or tiny drag). Work out which edge we were * closest to. */ int cx, cy; ui->ndragcoords = -1; /* * We process clicks based on the mouse-down location, * because that's more natural for a user to carefully * control than the mouse-up. */ x = ui->clickx; y = ui->clicky; gx = FROMCOORD(x); gy = FROMCOORD(y); cx = CENTERED_COORD(gx); cy = CENTERED_COORD(gy); if (!INGRID(state, gx, gy)) return MOVE_UI_UPDATE; if (max(abs(x-cx),abs(y-cy)) < TILE_SIZE/4) { /* TODO closer to centre of grid: process as a cell click not an edge click. */ return MOVE_UI_UPDATE; } else { int direction; if (abs(x-cx) < abs(y-cy)) { /* Closest to top/bottom edge. */ direction = (y < cy) ? U : D; } else { /* Closest to left/right edge. */ direction = (x < cx) ? L : R; } return mark_in_direction(state, gx, gy, direction, (button == LEFT_RELEASE), tmpbuf); } } } if (button == 'H' || button == 'h') return dupstr("H"); return MOVE_UNUSED; } static game_state *execute_move(const game_state *state, const char *move) { int w = state->shared->w, h = state->shared->h; char c; int x, y, l, n; game_state *ret = dup_game(state); debug(("move: %s\n", move)); while (*move) { c = *move; if (c == 'S') { ret->used_solve = true; move++; } else if (c == 'L' || c == 'N' || c == 'R' || c == 'F' || c == 'M') { /* 'line' or 'noline' or 'replace' or 'flip' or 'mark' */ move++; if (sscanf(move, "%d,%d,%d%n", &l, &x, &y, &n) != 3) goto badmove; if (!INGRID(state, x, y)) goto badmove; if (l < 0 || l > 15) goto badmove; if (c == 'L') ret->lines[y*w + x] |= (char)l; else if (c == 'N') ret->lines[y*w + x] &= ~((char)l); else if (c == 'R') { ret->lines[y*w + x] = (char)l; ret->marks[y*w + x] &= ~((char)l); /* erase marks too */ } else if (c == 'F') ret->lines[y*w + x] ^= (char)l; else if (c == 'M') ret->marks[y*w + x] ^= (char)l; /* * If we ended up trying to lay a line _over_ a mark, * that's a failed move: interpret_move() should have * ensured we never received a move string like that in * the first place. */ if ((ret->lines[y*w + x] & (char)l) && (ret->marks[y*w + x] & (char)l)) goto badmove; move += n; } else if (strcmp(move, "H") == 0) { pearl_solve(ret->shared->w, ret->shared->h, ret->shared->clues, ret->lines, DIFFCOUNT, true); for (n = 0; n < w*h; n++) ret->marks[n] &= ~ret->lines[n]; /* erase marks too */ move++; } else { goto badmove; } if (*move == ';') move++; else if (*move) goto badmove; } if (!check_completion(ret, true)) goto badmove; return ret; badmove: free_game(ret); return NULL; } /* ---------------------------------------------------------------------- * Drawing routines. */ #define FLASH_TIME 0.5F static void game_compute_size(const game_params *params, int tilesize, const game_ui *ui, int *x, int *y) { /* Ick: fake up `ds->tilesize' for macro expansion purposes */ struct { int halfsz; } ads, *ds = &ads; ads.halfsz = (tilesize-1)/2; *x = (params->w) * TILE_SIZE + 2 * BORDER; *y = (params->h) * TILE_SIZE + 2 * BORDER; } static void game_set_size(drawing *dr, game_drawstate *ds, const game_params *params, int tilesize) { ds->halfsz = (tilesize-1)/2; } static float *game_colours(frontend *fe, int *ncolours) { float *ret = snewn(3 * NCOLOURS, float); int i; game_mkhighlight(fe, ret, COL_BACKGROUND, COL_HIGHLIGHT, COL_LOWLIGHT); for (i = 0; i < 3; i++) { ret[COL_BLACK * 3 + i] = 0.0F; ret[COL_WHITE * 3 + i] = 1.0F; ret[COL_GRID * 3 + i] = 0.4F; } ret[COL_ERROR * 3 + 0] = 1.0F; ret[COL_ERROR * 3 + 1] = 0.0F; ret[COL_ERROR * 3 + 2] = 0.0F; ret[COL_DRAGON * 3 + 0] = 0.0F; ret[COL_DRAGON * 3 + 1] = 0.0F; ret[COL_DRAGON * 3 + 2] = 1.0F; ret[COL_DRAGOFF * 3 + 0] = 0.8F; ret[COL_DRAGOFF * 3 + 1] = 0.8F; ret[COL_DRAGOFF * 3 + 2] = 1.0F; ret[COL_FLASH * 3 + 0] = 1.0F; ret[COL_FLASH * 3 + 1] = 1.0F; ret[COL_FLASH * 3 + 2] = 1.0F; *ncolours = NCOLOURS; return ret; } static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state) { struct game_drawstate *ds = snew(struct game_drawstate); int i; ds->halfsz = 0; ds->started = false; ds->w = state->shared->w; ds->h = state->shared->h; ds->sz = state->shared->sz; ds->lflags = snewn(ds->sz, unsigned int); for (i = 0; i < ds->sz; i++) ds->lflags[i] = 0; ds->draglines = snewn(ds->sz, char); return ds; } static void game_free_drawstate(drawing *dr, game_drawstate *ds) { sfree(ds->draglines); sfree(ds->lflags); sfree(ds); } static void draw_lines_specific(drawing *dr, game_drawstate *ds, const game_ui *ui, int x, int y, unsigned int lflags, unsigned int shift, int c) { int ox = COORD(x), oy = COORD(y); int t2 = HALFSZ, t16 = HALFSZ/4; int cx = ox + t2, cy = oy + t2; int d; /* Draw each of the four directions, where laid (or error, or drag, etc.) */ for (d = 1; d < 16; d *= 2) { int xoff = t2 * DX(d), yoff = t2 * DY(d); int xnudge = abs(t16 * DX(C(d))), ynudge = abs(t16 * DY(C(d))); if ((lflags >> shift) & d) { int lx = cx + ((xoff < 0) ? xoff : 0) - xnudge; int ly = cy + ((yoff < 0) ? yoff : 0) - ynudge; if (c == COL_DRAGOFF && !(lflags & d)) continue; if (c == COL_DRAGON && (lflags & d)) continue; draw_rect(dr, lx, ly, abs(xoff)+2*xnudge+1, abs(yoff)+2*ynudge+1, c); /* end cap */ draw_rect(dr, cx - t16, cy - t16, 2*t16+1, 2*t16+1, c); } } } static void draw_square(drawing *dr, game_drawstate *ds, const game_ui *ui, int x, int y, unsigned int lflags, char clue) { int ox = COORD(x), oy = COORD(y); int t2 = HALFSZ, t16 = HALFSZ/4; int cx = ox + t2, cy = oy + t2; int d; assert(dr); /* Clip to the grid square. */ clip(dr, ox, oy, TILE_SIZE, TILE_SIZE); /* Clear the square. */ draw_rect(dr, ox, oy, TILE_SIZE, TILE_SIZE, (lflags & DS_CURSOR) ? COL_CURSOR_BACKGROUND : COL_BACKGROUND); if (ui->gui_style == GUI_LOOPY) { /* Draw small dot, underneath any lines. */ draw_circle(dr, cx, cy, t16, COL_GRID, COL_GRID); } else { /* Draw outline of grid square */ draw_line(dr, ox, oy, COORD(x+1), oy, COL_GRID); draw_line(dr, ox, oy, ox, COORD(y+1), COL_GRID); } /* Draw grid: either thin gridlines, or no-line marks. * We draw these first because the thick laid lines should be on top. */ for (d = 1; d < 16; d *= 2) { int xoff = t2 * DX(d), yoff = t2 * DY(d); if ((x == 0 && d == L) || (y == 0 && d == U) || (x == ds->w-1 && d == R) || (y == ds->h-1 && d == D)) continue; /* no gridlines out to the border. */ if ((lflags >> DS_MSHIFT) & d) { /* either a no-line mark ... */ int mx = cx + xoff, my = cy + yoff, msz = t16; draw_line(dr, mx-msz, my-msz, mx+msz, my+msz, COL_BLACK); draw_line(dr, mx-msz, my+msz, mx+msz, my-msz, COL_BLACK); } else { if (ui->gui_style == GUI_LOOPY) { /* draw grid lines connecting centre of cells */ draw_line(dr, cx, cy, cx+xoff, cy+yoff, COL_GRID); } } } /* Draw each of the four directions, where laid (or error, or drag, etc.) * Order is important here, specifically for the eventual colours of the * exposed end caps. */ draw_lines_specific(dr, ds, ui, x, y, lflags, 0, (lflags & DS_FLASH ? COL_FLASH : COL_BLACK)); draw_lines_specific(dr, ds, ui, x, y, lflags, DS_ESHIFT, COL_ERROR); draw_lines_specific(dr, ds, ui, x, y, lflags, DS_DSHIFT, COL_DRAGOFF); draw_lines_specific(dr, ds, ui, x, y, lflags, DS_DSHIFT, COL_DRAGON); /* Draw a clue, if present */ if (clue != NOCLUE) { int c = (lflags & DS_FLASH) ? COL_FLASH : (clue == STRAIGHT) ? COL_WHITE : COL_BLACK; if (lflags & DS_ERROR_CLUE) /* draw a bigger 'error' clue circle. */ draw_circle(dr, cx, cy, TILE_SIZE*3/8, COL_ERROR, COL_ERROR); draw_circle(dr, cx, cy, TILE_SIZE/4, c, COL_BLACK); } unclip(dr); draw_update(dr, ox, oy, TILE_SIZE, TILE_SIZE); } static void game_redraw(drawing *dr, game_drawstate *ds, const game_state *oldstate, const game_state *state, int dir, const game_ui *ui, float animtime, float flashtime) { int w = state->shared->w, h = state->shared->h, sz = state->shared->sz; int x, y, flashing = 0; bool force = false; if (!ds->started) { if (ui->gui_style == GUI_MASYU) { /* * Black rectangle which is the main grid. */ draw_rect(dr, BORDER - BORDER_WIDTH, BORDER - BORDER_WIDTH, w*TILE_SIZE + 2*BORDER_WIDTH + 1, h*TILE_SIZE + 2*BORDER_WIDTH + 1, COL_GRID); } draw_update(dr, 0, 0, w*TILE_SIZE + 2*BORDER, h*TILE_SIZE + 2*BORDER); ds->started = true; force = true; } if (flashtime > 0 && (flashtime <= FLASH_TIME/3 || flashtime >= FLASH_TIME*2/3)) flashing = DS_FLASH; memset(ds->draglines, 0, sz); if (ui->ndragcoords > 0) { int i; bool clearing = true; for (i = 0; i < ui->ndragcoords - 1; i++) { int sx, sy, dx, dy, dir, oldstate, newstate; interpret_ui_drag(state, ui, &clearing, i, &sx, &sy, &dx, &dy, &dir, &oldstate, &newstate); ds->draglines[sy*w+sx] ^= (oldstate ^ newstate); ds->draglines[dy*w+dx] ^= (F(oldstate) ^ F(newstate)); } } for (x = 0; x < w; x++) { for (y = 0; y < h; y++) { unsigned int f = (unsigned int)state->lines[y*w+x]; unsigned int eline = (unsigned int)(state->errors[y*w+x] & (R|U|L|D)); f |= eline << DS_ESHIFT; f |= ((unsigned int)ds->draglines[y*w+x]) << DS_DSHIFT; f |= ((unsigned int)state->marks[y*w+x]) << DS_MSHIFT; if (state->errors[y*w+x] & ERROR_CLUE) f |= DS_ERROR_CLUE; f |= flashing; if (ui->cursor_active && x == ui->curx && y == ui->cury) f |= DS_CURSOR; if (f != ds->lflags[y*w+x] || force) { ds->lflags[y*w+x] = f; draw_square(dr, ds, ui, x, y, f, state->shared->clues[y*w+x]); } } } } static float game_anim_length(const game_state *oldstate, const game_state *newstate, int dir, game_ui *ui) { return 0.0F; } static float game_flash_length(const game_state *oldstate, const game_state *newstate, int dir, game_ui *ui) { if (!oldstate->completed && newstate->completed && !oldstate->used_solve && !newstate->used_solve) return FLASH_TIME; else return 0.0F; } static void game_get_cursor_location(const game_ui *ui, const game_drawstate *ds, const game_state *state, const game_params *params, int *x, int *y, int *w, int *h) { if(ui->cursor_active) { *x = COORD(ui->curx); *y = COORD(ui->cury); *w = *h = TILE_SIZE; } } static int game_status(const game_state *state) { return state->completed ? +1 : 0; } static void game_print_size(const game_params *params, const game_ui *ui, float *x, float *y) { int pw, ph; /* * I'll use 6mm squares by default. */ game_compute_size(params, 600, ui, &pw, &ph); *x = pw / 100.0F; *y = ph / 100.0F; } static void game_print(drawing *dr, const game_state *state, const game_ui *ui, int tilesize) { int w = state->shared->w, h = state->shared->h, x, y; int black = print_mono_colour(dr, 0); int white = print_mono_colour(dr, 1); /* Ick: fake up `ds->tilesize' for macro expansion purposes */ game_drawstate *ds = game_new_drawstate(dr, state); game_set_size(dr, ds, NULL, tilesize); if (ui->gui_style == GUI_MASYU) { /* Draw grid outlines (black). */ for (x = 0; x <= w; x++) draw_line(dr, COORD(x), COORD(0), COORD(x), COORD(h), black); for (y = 0; y <= h; y++) draw_line(dr, COORD(0), COORD(y), COORD(w), COORD(y), black); } else { /* Draw small dots, and dotted lines connecting them. For * added clarity, try to start and end the dotted lines a * little way away from the dots. */ print_line_width(dr, TILE_SIZE / 40); print_line_dotted(dr, true); for (x = 0; x < w; x++) { for (y = 0; y < h; y++) { int cx = COORD(x) + HALFSZ, cy = COORD(y) + HALFSZ; draw_circle(dr, cx, cy, tilesize/10, black, black); if (x+1 < w) draw_line(dr, cx+tilesize/5, cy, cx+tilesize-tilesize/5, cy, black); if (y+1 < h) draw_line(dr, cx, cy+tilesize/5, cx, cy+tilesize-tilesize/5, black); } } print_line_dotted(dr, false); } for (x = 0; x < w; x++) { for (y = 0; y < h; y++) { int cx = COORD(x) + HALFSZ, cy = COORD(y) + HALFSZ; int clue = state->shared->clues[y*w+x]; draw_lines_specific(dr, ds, ui, x, y, state->lines[y*w+x], 0, black); if (clue != NOCLUE) { int c = (clue == CORNER) ? black : white; draw_circle(dr, cx, cy, TILE_SIZE/4, c, black); } } } game_free_drawstate(dr, ds); } #ifdef COMBINED #define thegame pearl #endif const struct game thegame = { "Pearl", "games.pearl", "pearl", default_params, game_fetch_preset, NULL, decode_params, encode_params, free_params, dup_params, true, game_configure, custom_params, validate_params, new_game_desc, validate_desc, new_game, dup_game, free_game, true, solve_game, true, game_can_format_as_text_now, game_text_format, get_prefs, set_prefs, new_ui, free_ui, NULL, /* encode_ui */ NULL, /* decode_ui */ NULL, /* game_request_keys */ game_changed_state, current_key_label, interpret_move, execute_move, PREFERRED_TILE_SIZE, game_compute_size, game_set_size, game_colours, game_new_drawstate, game_free_drawstate, game_redraw, game_anim_length, game_flash_length, game_get_cursor_location, game_status, true, false, game_print_size, game_print, false, /* wants_statusbar */ false, NULL, /* timing_state */ 0, /* flags */ }; #ifdef STANDALONE_SOLVER #include <time.h> #include <stdarg.h> static const char *quis = NULL; static void usage(FILE *out) { fprintf(out, "usage: %s <params>\n", quis); } static void pnum(int n, int ntot, const char *desc) { printf("%2.1f%% (%d) %s", (double)n*100.0 / (double)ntot, n, desc); } static void start_soak(game_params *p, random_state *rs, int nsecs) { time_t tt_start, tt_now, tt_last; int n = 0, nsolved = 0, nimpossible = 0, ret; char *grid, *clues; tt_start = tt_last = time(NULL); /* Currently this generates puzzles of any difficulty (trying to solve it * on the maximum difficulty level and not checking it's not too easy). */ printf("Soak-testing a %dx%d grid (any difficulty)", p->w, p->h); if (nsecs > 0) printf(" for %d seconds", nsecs); printf(".\n"); p->nosolve = true; grid = snewn(p->w*p->h, char); clues = snewn(p->w*p->h, char); while (1) { n += new_clues(p, rs, clues, grid); /* should be 1, with nosolve */ ret = pearl_solve(p->w, p->h, clues, grid, DIFF_TRICKY, false); if (ret <= 0) nimpossible++; if (ret == 1) nsolved++; tt_now = time(NULL); if (tt_now > tt_last) { tt_last = tt_now; printf("%d total, %3.1f/s, ", n, (double)n / ((double)tt_now - tt_start)); pnum(nsolved, n, "solved"); printf(", "); printf("%3.1f/s", (double)nsolved / ((double)tt_now - tt_start)); if (nimpossible > 0) pnum(nimpossible, n, "impossible"); printf("\n"); } if (nsecs > 0 && (tt_now - tt_start) > nsecs) { printf("\n"); break; } } sfree(grid); sfree(clues); } int main(int argc, char *argv[]) { game_params *p = NULL; random_state *rs = NULL; time_t seed = time(NULL); char *id = NULL; const char *err; setvbuf(stdout, NULL, _IONBF, 0); quis = argv[0]; while (--argc > 0) { char *p = (char*)(*++argv); if (!strcmp(p, "-e") || !strcmp(p, "--seed")) { seed = atoi(*++argv); argc--; } else if (*p == '-') { fprintf(stderr, "%s: unrecognised option `%s'\n", argv[0], p); usage(stderr); exit(1); } else { id = p; } } rs = random_new((void*)&seed, sizeof(time_t)); p = default_params(); if (id) { if (strchr(id, ':')) { fprintf(stderr, "soak takes params only.\n"); goto done; } decode_params(p, id); err = validate_params(p, true); if (err) { fprintf(stderr, "%s: %s", argv[0], err); goto done; } start_soak(p, rs, 0); /* run forever */ } else { int i; for (i = 5; i <= 12; i++) { p->w = p->h = i; start_soak(p, rs, 5); } } done: free_params(p); random_free(rs); return 0; } #endif /* vim: set shiftwidth=4 tabstop=8: */