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.
This commit is contained in:
Franklin Wei
2024-07-23 05:49:04 -04:00
committed by Simon Tatham
parent 72ff127404
commit 62c0e0dcc9
2 changed files with 170 additions and 24 deletions

View File

@ -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 */

View File

@ -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}