X-Git-Url: https://git.distorted.org.uk/~mdw/sgt/puzzles/blobdiff_plain/b760b8bdf1ff5b603e3076c8e21cf246d08937f7..63e20fbe30306376edb2c0aadfc1ea386ebdb4e0:/pearl.c diff --git a/pearl.c b/pearl.c index c018d5a..40c32eb 100644 --- a/pearl.c +++ b/pearl.c @@ -5,10 +5,17 @@ /* * TODO: * - * - Keyboard-control cursor. (Would probably have to address both - * square centres, for laying multiple edges at a time in a - * drag-like style, and grid edges for marking particular line - * segments as no-go.) + * - 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 @@ -83,6 +90,7 @@ 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, @@ -1153,9 +1161,17 @@ void pearl_loopgen(int w, int h, char *lines, random_state *rs) static int new_clues(game_params *params, random_state *rs, char *clues, char *grid) { - int w = params->w, h = params->h; + 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); @@ -1237,7 +1253,7 @@ static int new_clues(game_params *params, random_state *rs, /* * See if we can solve the puzzle just like this. */ - ret = pearl_solve(w, h, clues, grid, params->difficulty, FALSE); + 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 */ @@ -1245,8 +1261,8 @@ static int new_clues(game_params *params, random_state *rs, /* * Check this puzzle isn't too easy. */ - if (params->difficulty > DIFF_EASY) { - ret = pearl_solve(w, h, clues, grid, params->difficulty-1, FALSE); + 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 */ @@ -1313,7 +1329,7 @@ static int new_clues(game_params *params, random_state *rs, clue = clues[y*w+x]; clues[y*w+x] = 0; /* try removing this clue */ - ret = pearl_solve(w, h, clues, grid, params->difficulty, FALSE); + 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 */ @@ -1335,6 +1351,8 @@ static int new_clues(game_params *params, random_state *rs, break; /* got it */ } + debug(("%d %dx%d loops before finished puzzle.\n", ngen, w, h)); + return ngen; } @@ -1343,14 +1361,12 @@ static char *new_game_desc(game_params *params, random_state *rs, { char *grid, *clues; char *desc; - int ngen, w = params->w, h = params->h, i, j; + int w = params->w, h = params->h, i, j; grid = snewn(w*h, char); clues = snewn(w*h, char); - ngen = new_clues(params, rs, clues, grid); - - debug(("%d %dx%d loops before finished puzzle.\n", ngen, w, h)); + new_clues(params, rs, clues, grid); desc = snewn(w * h + 1, char); for (i = j = 0; i < w*h; i++) { @@ -1717,8 +1733,12 @@ static char *game_text_format(game_state *state) struct game_ui { int *dragcoords; /* list of (y*w+x) coords in drag so far */ - int ndragcoords; /* number of entries in dragcoords. 0 = no drag. */ + 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 */ + int cursor_active; /* TRUE iff cursor is shown */ }; static game_ui *new_ui(game_state *state) @@ -1726,8 +1746,10 @@ static game_ui *new_ui(game_state *state) game_ui *ui = snew(game_ui); int sz = state->shared->sz; - ui->ndragcoords = 0; + ui->ndragcoords = -1; ui->dragcoords = snewn(sz, int); + ui->cursor_active = FALSE; + ui->curx = ui->cury = 0; return ui; } @@ -1761,6 +1783,7 @@ static void game_changed_state(game_ui *ui, game_state *oldstate, #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 */ @@ -1769,6 +1792,7 @@ static void game_changed_state(game_ui *ui, game_state *oldstate, #define DS_ERROR_CLUE (1 << 20) #define DS_FLASH (1 << 21) +#define DS_CURSOR (1 << 22) enum { GUI_MASYU, GUI_LOOPY }; @@ -1805,6 +1829,9 @@ static void update_ui_drag(game_state *state, game_ui *ui, int gx, int gy) 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]; @@ -1836,7 +1863,15 @@ static void update_ui_drag(game_state *state, game_ui *ui, int gx, int gy) 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; @@ -1900,15 +1935,48 @@ static void interpret_ui_drag(game_state *state, game_ui *ui, int *clearing, } } +static char *mark_in_direction(game_state *state, int x, int y, int dir, + int ismark, 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 = ismark ? 'M' : 'F'; + + if (!INGRID(state, x, y) || !INGRID(state, x2, y2)) return ""; + /* disallow laying a mark over a line, or vice versa. */ + if (ismark) { + if ((state->lines[y*w+x] & dir) || (state->lines[y2*w+x2] & dir2)) + return ""; + } else { + if ((state->marks[y*w+x] & dir) || (state->marks[y2*w+x2] & dir2)) + return ""; + } + + 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(game_state *state, game_ui *ui, game_drawstate *ds, int x, int y, int button) { - int w = state->shared->w /*, h = state->shared->h, sz = state->shared->sz */; + int w = state->shared->w, h = state->shared->h /*, sz = state->shared->sz */; int gx = FROMCOORD(x), gy = FROMCOORD(y), i; + int release = FALSE; char tmpbuf[80]; - if (button == LEFT_BUTTON) { - if (!INGRID(state, gx, gy)) return NULL; + if (IS_MOUSE_DOWN(button)) { + ui->cursor_active = FALSE; + + if (!INGRID(state, gx, gy)) { + ui->ndragcoords = -1; + return NULL; + } ui->clickx = x; ui->clicky = y; ui->dragcoords[0] = gy * w + gx; @@ -1917,13 +1985,50 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds, return ""; } - if (IS_MOUSE_DRAG(button)) { + if (button == LEFT_DRAG && ui->ndragcoords >= 0) { update_ui_drag(state, ui, gx, gy); return ""; } - if (IS_MOUSE_RELEASE(button)) { - if (ui->ndragcoords) { + if (IS_MOUSE_RELEASE(button)) release = TRUE; + + if (IS_CURSOR_MOVE(button & ~MOD_MASK)) { + if (!ui->cursor_active) { + ui->cursor_active = TRUE; + } else if (button & (MOD_SHFT | MOD_CTRL)) { + if (ui->ndragcoords > 0) return NULL; + ui->ndragcoords = -1; + return mark_in_direction(state, ui->curx, ui->cury, + KEY_DIRECTION(button & ~MOD_MASK), + (button & MOD_SHFT), tmpbuf); + } 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 ""; + } + + if (IS_CURSOR_SELECT(button & ~MOD_MASK)) { + if (!ui->cursor_active) { + ui->cursor_active = TRUE; + return ""; + } 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 ""; + } else release = TRUE; + } else if (button == CURSOR_SELECT2 && ui->ndragcoords >= 0) { + ui->ndragcoords = -1; + return ""; + } + } + + if (release) { + if (ui->ndragcoords > 0) { /* End of a drag: process the cached line data. */ int buflen = 0, bufsize = 256, tmplen; char *buf = NULL; @@ -1949,14 +2054,28 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds, } } - ui->ndragcoords = 0; + ui->ndragcoords = -1; return buf ? buf : ""; - } else { - /* Click (or tiny drag). Work out which edge we were closest to. */ - int cx = COORD(gx) + TILE_SIZE/2, cy = COORD(gy) + TILE_SIZE/2; - int gx2, gy2, l1, l2, ismark = (button == RIGHT_RELEASE); - char movec = ismark ? 'M' : 'F'; + } 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 ""; @@ -1965,30 +2084,16 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds, return ""; } else { + int direction; if (abs(x-cx) < abs(y-cy)) { /* Closest to top/bottom edge. */ - l1 = (y < cy) ? U : D; + direction = (y < cy) ? U : D; } else { /* Closest to left/right edge. */ - l1 = (x < cx) ? L : R; + direction = (x < cx) ? L : R; } - gx2 = gx + DX(l1); gy2 = gy + DY(l1); - l2 = F(l1); - - if (!INGRID(state, gx, gy) || !INGRID(state, gx2, gy2)) return ""; - - /* disallow laying a mark over a line, or vice versa. */ - if (ismark) { - if ((state->lines[gy*w+gx] & l1) || (state->lines[gy2*w+gx2] & l2)) - return ""; - } else { - if ((state->marks[gy*w+gx] & l1) || (state->marks[gy2*w+gx2] & l2)) - return ""; - } - - sprintf(tmpbuf, "%c%d,%d,%d;%c%d,%d,%d", - movec, l1, gx, gy, movec, l2, gx2, gy2); - return dupstr(tmpbuf); + return mark_in_direction(state, gx, gy, direction, + (button == RIGHT_RELEASE), tmpbuf); } } } @@ -1996,8 +2101,6 @@ static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds, if (button == 'H' || button == 'h') return dupstr("H"); - /* TODO cursor */ - return NULL; } @@ -2023,9 +2126,6 @@ static game_state *execute_move(game_state *state, char *move) if (!INGRID(state, x, y)) goto badmove; if (l < 0 || l > 15) goto badmove; - /* TODO trying to set a line over a no-line mark should be - * a failed move? */ - if (c == 'L') ret->lines[y*w + x] |= (char)l; else if (c == 'N') @@ -2038,6 +2138,16 @@ static game_state *execute_move(game_state *state, char *move) 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, @@ -2193,7 +2303,10 @@ static void draw_square(drawing *dr, game_drawstate *ds, game_ui *ui, clip(dr, ox, oy, TILE_SIZE, TILE_SIZE); /* Clear the square. */ - draw_rect(dr, ox, oy, TILE_SIZE, TILE_SIZE, COL_BACKGROUND); + draw_rect(dr, ox, oy, TILE_SIZE, TILE_SIZE, + (lflags & DS_CURSOR) ? + COL_CURSOR_BACKGROUND : COL_BACKGROUND); + if (get_gui_style() == GUI_LOOPY) { /* Draw small dot, underneath any lines. */ @@ -2241,7 +2354,7 @@ static void draw_square(drawing *dr, game_drawstate *ds, game_ui *ui, /* Draw a clue, if present */ if (clue != NOCLUE) { int c = (lflags & DS_FLASH) ? COL_FLASH : - (clue == CORNER) ? COL_BLACK : COL_WHITE; + (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); @@ -2292,7 +2405,7 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate, flashing = DS_FLASH; memset(ds->draglines, 0, sz); - if (ui->dragcoords) { + if (ui->ndragcoords > 0) { int i, clearing = TRUE; for (i = 0; i < ui->ndragcoords - 1; i++) { int sx, sy, dx, dy, dir, oldstate, newstate; @@ -2317,6 +2430,9 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate, 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]);