From 62c0e0dcc9e7e250f1936b107e5b76e881b69496 Mon Sep 17 00:00:00 2001 From: Franklin Wei Date: Tue, 23 Jul 2024 05:49:04 -0400 Subject: [PATCH] Palisade: add double-resolution cursor mode. This adds a "half-grid" cursor mode, settable via a preference, which doubles the resolution of the keyboard cursor, so that it can be over a square center, vertex, or edge. The cursor select buttons then toggle the edge directly under the cursor. There are two advantages to this new behavior. First, the game can now be played with only the cursor keys, doing away with the need to hold Control or Shift, which are not currently emulated on Rockbox. And second, the new interface is arguably more discoverable than the legacy mode, which is still retained as a user preference. --- palisade.c | 174 ++++++++++++++++++++++++++++++++++++++++++++++------ puzzles.but | 20 ++++-- 2 files changed, 170 insertions(+), 24 deletions(-) diff --git a/palisade.c b/palisade.c index 811204b..ecbbbb4 100644 --- a/palisade.c +++ b/palisade.c @@ -868,18 +868,48 @@ static char *game_text_format(const game_state *state) } struct game_ui { + /* These are half-grid coordinates - (0,0) is the top left corner + * of the top left square; (1,1) is the center of the top left + * grid square. */ int x, y; bool show; + + bool legacy_cursor; }; static game_ui *new_ui(const game_state *state) { game_ui *ui = snew(game_ui); - ui->x = ui->y = 0; + ui->x = ui->y = 1; ui->show = getenv_bool("PUZZLES_SHOW_CURSOR", false); + ui->legacy_cursor = false; return ui; } +static config_item *get_prefs(game_ui *ui) +{ + config_item *cfg; + + cfg = snewn(2, config_item); + + cfg[0].name = "Cursor mode"; + cfg[0].kw = "cursor-mode"; + cfg[0].type = C_CHOICES; + cfg[0].u.choices.choicenames = ":Half-grid:Full-grid"; + cfg[0].u.choices.choicekws = ":half:full"; + cfg[0].u.choices.selected = ui->legacy_cursor; + + cfg[1].name = NULL; + cfg[1].type = C_END; + + return cfg; +} + +static void set_prefs(game_ui *ui, const config_item *cfg) +{ + ui->legacy_cursor = cfg[0].u.choices.selected; +} + static void free_ui(game_ui *ui) { sfree(ui); @@ -890,7 +920,7 @@ static void game_changed_state(game_ui *ui, const game_state *oldstate, { } -typedef unsigned short dsflags; +typedef int dsflags; struct game_drawstate { int tilesize; @@ -921,9 +951,6 @@ static char *interpret_move(const game_state *state, game_ui *ui, if (OUT_OF_BOUNDS(gx, gy, w, h)) return NULL; - ui->x = gx; - ui->y = gy; - /* find edge closest to click point */ possible &=~ (2*px < TILESIZE ? BORDER_R : BORDER_L); possible &=~ (2*py < TILESIZE ? BORDER_D : BORDER_U); @@ -934,6 +961,9 @@ static char *interpret_move(const game_state *state, game_ui *ui, for (dir = 0; dir < 4 && BORDER(dir) != possible; ++dir); if (dir == 4) return NULL; /* there's not exactly one such edge */ + ui->x = min(max(2*gx + 1 + dx[dir], 1), 2*w-1); + ui->y = min(max(2*gy + 1 + dy[dir], 1), 2*h-1); + hx = gx + dx[dir]; hy = gy + dy[dir]; @@ -963,17 +993,17 @@ static char *interpret_move(const game_state *state, game_ui *ui, } if (IS_CURSOR_MOVE(button)) { - if (control || shift) { + if(ui->legacy_cursor && (control || shift)) { borderflag flag = 0, newflag; - int dir, i = ui->y * w + ui->x; + int dir, i = (ui->y/2) * w + (ui->x/2); ui->show = true; - x = ui->x; - y = ui->y; + x = ui->x/2; + y = ui->y/2; move_cursor(button, &x, &y, w, h, false, NULL); if (OUT_OF_BOUNDS(x, y, w, h)) return NULL; for (dir = 0; dir < 4; ++dir) - if (dx[dir] == x - ui->x && dy[dir] == y - ui->y) break; + if (dx[dir] == x - ui->x/2 && dy[dir] == y - ui->y/2) break; if (dir == 4) return NULL; /* how the ... ?! */ if (control) flag |= BORDER(dir); @@ -987,9 +1017,67 @@ static char *interpret_move(const game_state *state, game_ui *ui, if (control) newflag |= BORDER(FLIP(dir)); if (shift) newflag |= DISABLED(BORDER(FLIP(dir))); return string(80, "F%d,%d,%dF%d,%d,%d", - ui->x, ui->y, flag, x, y, newflag); - } else - return move_cursor(button, &ui->x, &ui->y, w, h, false, &ui->show); + ui->x/2, ui->y/2, flag, x, y, newflag); + } else { + /* TODO: Refactor this and other half-grid cursor games + * (Tracks, etc.) */ + int dx = (button == CURSOR_LEFT) ? -1 : ((button == CURSOR_RIGHT) ? +1 : 0); + int dy = (button == CURSOR_DOWN) ? +1 : ((button == CURSOR_UP) ? -1 : 0); + + if(ui->legacy_cursor) { + dx *= 2; dy *= 2; + + ui->x |= 1; + ui->y |= 1; + } + + if (!ui->show) { + ui->show = true; + } + + ui->x = min(max(ui->x + dx, 1), 2*w-1); + ui->y = min(max(ui->y + dy, 1), 2*h-1); + + return MOVE_UI_UPDATE; + } + } else if (IS_CURSOR_SELECT(button)) { + int px = ui->x % 2, py = ui->y % 2; + int gx = ui->x / 2, gy = ui->y / 2; + int dir = (px == 0) ? 3 : 0; /* left = 3; up = 0 */ + int hx = gx + dx[dir]; + int hy = gy + dy[dir]; + + int i = gy * w + gx; + + if(!ui->show) { + ui->show = true; + return MOVE_UI_UPDATE; + } + + /* clicks on square corners and centers do nothing */ + if (px == py) + return MOVE_NO_EFFECT; + + /* TODO: Refactor this and the mouse click handling code + * above. */ + switch ((button == CURSOR_SELECT2) | + ((state->borders[i] & BORDER(dir)) >> dir << 1) | + ((state->borders[i] & DISABLED(BORDER(dir))) >> dir >> 2)) { + + case MAYBE_LEFT: + case ON_LEFT: + case ON_RIGHT: + return string(80, "F%d,%d,%dF%d,%d,%d", + gx, gy, BORDER(dir), + hx, hy, BORDER(FLIP(dir))); + + case MAYBE_RIGHT: + case OFF_LEFT: + case OFF_RIGHT: + return string(80, "F%d,%d,%dF%d,%d,%d", + gx, gy, DISABLED(BORDER(dir)), + hx, hy, DISABLED(BORDER(FLIP(dir)))); + } } return NULL; @@ -1098,7 +1186,7 @@ static float *game_colours(frontend *fe, int *ncolours) #define F_ERROR_L BORDER_ERROR(BORDER_L) /* BIT(11) */ #define F_ERROR_CLUE BIT(12) #define F_FLASH BIT(13) -#define F_CURSOR BIT(14) +#define CONTAINS_CURSOR(x) ((x) << 14) static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state) { @@ -1132,9 +1220,6 @@ static void draw_tile(drawing *dr, game_drawstate *ds, int r, int c, draw_rect(dr, x + WIDTH, y + WIDTH, TILESIZE - WIDTH, TILESIZE - WIDTH, (flags & F_FLASH ? COL_FLASH : COL_BACKGROUND)); - if (flags & F_CURSOR) - draw_rect_corners(dr, x + CENTER, y + CENTER, TILESIZE / 3, COL_GRID); - if (clue != EMPTY) { char buf[2]; buf[0] = '0' + clue; @@ -1158,6 +1243,47 @@ static void draw_tile(drawing *dr, game_drawstate *ds, int r, int c, draw_update(dr, x, y, TILESIZE + WIDTH, TILESIZE + WIDTH); } +static void draw_cursor(drawing *dr, game_drawstate *ds, + int cur_x, int cur_y, bool legacy_cursor) +{ + int off_x = cur_x % 2, off_y = cur_y % 2; + + /* Figure out the tile coordinates corresponding to these cursor + * coordinates. */ + int x = MARGIN + TILESIZE * (cur_x / 2), y = MARGIN + TILESIZE * (cur_y / 2); + + /* off_x and off_y are either 0 or 1. The possible cases are + * therefore: + * + * (0, 0): the cursor is in the top left corner of the tile. + * (0, 1): the cursor is on the left border of the tile. + * (1, 0): the cursor is on the top border of the tile. + * (1, 1): the cursor is in the center of the tile. + */ + enum { TOP_LEFT_CORNER, LEFT_BORDER, TOP_BORDER, TILE_CENTER } cur_type = (off_x << 1) + off_y; + + int center_x = x + ((off_x == 0) ? WIDTH/2 : CENTER), + center_y = y + ((off_y == 0) ? WIDTH/2 : CENTER); + + struct { int w, h; } cursor_dimensions[] = { + { TILESIZE / 3, TILESIZE / 3 }, /* top left corner */ + { TILESIZE / 3, 2 * TILESIZE / 3}, /* left border */ + { 2 * TILESIZE / 3, TILESIZE / 3}, /* top border */ + { 2 * TILESIZE / 3, 2 * TILESIZE / 3 } /* center */ + }, *dims = cursor_dimensions + cur_type; + + if(legacy_cursor && cur_type == TILE_CENTER) + draw_rect_corners(dr, center_x, center_y, TILESIZE / 3, COL_GRID); + else + draw_rect_outline(dr, + center_x - dims->w / 2, center_y - dims->h / 2, + dims->w, dims->h, COL_GRID); + + draw_update(dr, + center_x - dims->w / 2, center_y - dims->h / 2, + dims->w, dims->h); +} + #define FLASH_TIME 0.7F static void game_redraw(drawing *dr, game_drawstate *ds, @@ -1203,8 +1329,13 @@ static void game_redraw(drawing *dr, game_drawstate *ds, if (clue != EMPTY && (on > clue || clue > 4 - off)) flags |= F_ERROR_CLUE; - if (ui->show && ui->x == c && ui->y == r) - flags |= F_CURSOR; + if (ui->show) { + int u, v; + for(u = 0; u < 3; u++) + for(v = 0; v < 3; v++) + if(ui->x == 2*c+u && ui->y == 2*r+v) + flags |= CONTAINS_CURSOR(BIT(3*u+v)); + } /* border errors */ for (dir = 0; dir < 4; ++dir) { @@ -1248,6 +1379,9 @@ static void game_redraw(drawing *dr, game_drawstate *ds, draw_tile(dr, ds, r, c, ds->grid[i], clue); } + if (ui->show) + draw_cursor(dr, ds, ui->x, ui->y, ui->legacy_cursor); + dsf_free(black_border_dsf); dsf_free(yellow_border_dsf); } @@ -1375,7 +1509,7 @@ const struct game thegame = { free_game, true, solve_game, true, game_can_format_as_text_now, game_text_format, - NULL, NULL, /* get_prefs, set_prefs */ + get_prefs, set_prefs, /* get_prefs, set_prefs */ new_ui, free_ui, NULL, /* encode_ui */ diff --git a/puzzles.but b/puzzles.but index 8cc18e2..0eb3511 100644 --- a/puzzles.but +++ b/puzzles.but @@ -3519,10 +3519,15 @@ Palisade was contributed to this collection by Jonas K\u00F6{oe}lker. \H{palisade-controls} \I{controls, for Palisade}Palisade controls Left-click to place an edge. Right-click to indicate \q{no edge}. -Alternatively, the arrow keys will move a keyboard cursor. Holding -Control while pressing an arrow key will place an edge. Press -Shift-arrowkey to switch off an edge. Repeat an action to perform -its inverse. + +Alternatively, the arrow keys will move a keyboard cursor. Depending +on the \q{Cursor mode} preference (see \k{palisade-prefs}), the cursor +will either navigate among the grid squares, or along their +borders. In \q{Full-grid} mode, hold Control while pressing an arrow +key to place an edge, and press Shift-arrowkey to switch off an +edge. In \q{Half-grid} mode, press Enter to place an edge, and Space +to switch off an edge. In either mode, you can repeat an action to +perform its inverse. (All the actions described in \k{common-actions} are also available.) @@ -3539,6 +3544,13 @@ These parameters are available from the \q{Custom...} option on the \dd The size of the regions into which the grid must be subdivided. +\H{palisade-prefs} \I{preferences, for Palisade}Palisade user preferences + +On platforms that support user preferences, the \q{Preferences} option +on the \q{Game} menu will let you configure the behavior of the cursor +keys to either navigate among full grid squares, or along the borders +of the grid squares. + \C{mosaic} \i{Mosaic} \cfg{winhelp-topic}{games.mosaic}