Dominosa: introduce a difficulty system.

Currently, there are just two difficulty levels. 'Basic' is the same
as the old solver; 'Trivial' is the subset that guarantees the puzzle
can be solved using only the two simplest deductions of all: 'this
domino can only go in one place' and 'only one domino orientation can
fit in this square'.

The solver has also acquired a -g option, to grade the difficulty of
an input puzzle using this system.
This commit is contained in:
Simon Tatham
2019-04-02 21:01:36 +01:00
parent 82ee3d42a4
commit 6235f7fb3d

View File

@ -84,6 +84,24 @@
#define FLASH_TIME 0.13F #define FLASH_TIME 0.13F
/*
* Difficulty levels. I do some macro ickery here to ensure that my
* enum and the various forms of my name list always match up.
*/
#define DIFFLIST(X) \
X(TRIVIAL,Trivial,t) \
X(BASIC,Basic,b) \
X(AMBIGUOUS,Ambiguous,a) \
/* end of list */
#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 dominosa_diffnames[] = { DIFFLIST(TITLE) };
static char const dominosa_diffchars[] = DIFFLIST(ENCODE);
#define DIFFCONFIG DIFFLIST(CONFIG)
enum { enum {
COL_BACKGROUND, COL_BACKGROUND,
COL_TEXT, COL_TEXT,
@ -98,7 +116,7 @@ enum {
struct game_params { struct game_params {
int n; int n;
bool unique; int diff;
}; };
struct game_numbers { struct game_numbers {
@ -125,35 +143,39 @@ static game_params *default_params(void)
game_params *ret = snew(game_params); game_params *ret = snew(game_params);
ret->n = 6; ret->n = 6;
ret->unique = true; ret->diff = DIFF_BASIC;
return ret; return ret;
} }
static bool game_fetch_preset(int i, char **name, game_params **params) static const struct game_params dominosa_presets[] = {
{ 3, DIFF_TRIVIAL },
{ 4, DIFF_TRIVIAL },
{ 5, DIFF_TRIVIAL },
{ 6, DIFF_TRIVIAL },
{ 4, DIFF_BASIC },
{ 5, DIFF_BASIC },
{ 6, DIFF_BASIC },
{ 7, DIFF_BASIC },
{ 8, DIFF_BASIC },
{ 9, DIFF_BASIC },
};
static bool game_fetch_preset(int i, char **name, game_params **params_out)
{ {
game_params *ret; game_params *params;
int n;
char buf[80]; char buf[80];
switch (i) { if (i < 0 || i >= lenof(dominosa_presets))
case 0: n = 3; break; return false;
case 1: n = 4; break;
case 2: n = 5; break; params = snew(game_params);
case 3: n = 6; break; *params = dominosa_presets[i]; /* structure copy */
case 4: n = 7; break;
case 5: n = 8; break; sprintf(buf, "Order %d, %s", params->n, dominosa_diffnames[params->diff]);
case 6: n = 9; break;
default: return false;
}
sprintf(buf, "Up to double-%d", n);
*name = dupstr(buf); *name = dupstr(buf);
*params_out = params;
*params = ret = snew(game_params);
ret->n = n;
ret->unique = true;
return true; return true;
} }
@ -171,18 +193,36 @@ static game_params *dup_params(const game_params *params)
static void decode_params(game_params *params, char const *string) static void decode_params(game_params *params, char const *string)
{ {
params->n = atoi(string); const char *p = string;
while (*string && isdigit((unsigned char)*string)) string++;
if (*string == 'a') params->n = atoi(p);
params->unique = false; while (*p && isdigit((unsigned char)*p)) p++;
while (*p) {
char c = *p++;
if (c == 'a') {
/* Legacy encoding from before the difficulty system */
params->diff = DIFF_AMBIGUOUS;
} else if (c == 'd') {
int i;
params->diff = DIFFCOUNT+1; /* ...which is invalid */
if (*p) {
for (i = 0; i < DIFFCOUNT; i++) {
if (*p == dominosa_diffchars[i])
params->diff = i;
}
p++;
}
}
}
} }
static char *encode_params(const game_params *params, bool full) static char *encode_params(const game_params *params, bool full)
{ {
char buf[80]; char buf[80];
sprintf(buf, "%d", params->n); int len = sprintf(buf, "%d", params->n);
if (full && !params->unique) if (full)
strcat(buf, "a"); len += sprintf(buf + len, "d%c", dominosa_diffchars[params->diff]);
return dupstr(buf); return dupstr(buf);
} }
@ -198,9 +238,10 @@ static config_item *game_configure(const game_params *params)
sprintf(buf, "%d", params->n); sprintf(buf, "%d", params->n);
ret[0].u.string.sval = dupstr(buf); ret[0].u.string.sval = dupstr(buf);
ret[1].name = "Ensure unique solution"; ret[1].name = "Difficulty";
ret[1].type = C_BOOLEAN; ret[1].type = C_CHOICES;
ret[1].u.boolean.bval = params->unique; ret[1].u.choices.choicenames = DIFFCONFIG;
ret[1].u.choices.selected = params->diff;
ret[2].name = NULL; ret[2].name = NULL;
ret[2].type = C_END; ret[2].type = C_END;
@ -213,7 +254,7 @@ static game_params *custom_params(const config_item *cfg)
game_params *ret = snew(game_params); game_params *ret = snew(game_params);
ret->n = atoi(cfg[0].u.string.sval); ret->n = atoi(cfg[0].u.string.sval);
ret->unique = cfg[1].u.boolean.bval; ret->diff = cfg[1].u.choices.selected;
return ret; return ret;
} }
@ -222,6 +263,8 @@ static const char *validate_params(const game_params *params, bool full)
{ {
if (params->n < 1) if (params->n < 1)
return "Maximum face number must be at least one"; return "Maximum face number must be at least one";
if (params->diff >= DIFFCOUNT)
return "Unknown difficulty rating";
return NULL; return NULL;
} }
@ -305,6 +348,7 @@ struct solver_square {
struct solver_scratch { struct solver_scratch {
int n, dc, pc, w, h, wh; int n, dc, pc, w, h, wh;
int max_diff_used;
struct solver_domino *dominoes; struct solver_domino *dominoes;
struct solver_placement *placements; struct solver_placement *placements;
struct solver_square *squares; struct solver_square *squares;
@ -491,6 +535,8 @@ static void solver_setup_grid(struct solver_scratch *sc, const int *numbers)
sq->placements[sq->nplacements++] = p; sq->placements[sq->nplacements++] = p;
} }
} }
sc->max_diff_used = DIFF_TRIVIAL;
} }
static void rule_out_placement( static void rule_out_placement(
@ -707,7 +753,7 @@ static bool deduce_domino_must_overlap(struct solver_scratch *sc, int di)
* 2 = multiple possibilities remain (puzzle is ambiguous or solver is not * 2 = multiple possibilities remain (puzzle is ambiguous or solver is not
* smart enough) * smart enough)
*/ */
static int run_solver(struct solver_scratch *sc) static int run_solver(struct solver_scratch *sc, int max_diff_allowed)
{ {
int di, si; int di, si;
bool done_something; bool done_something;
@ -732,28 +778,37 @@ static int run_solver(struct solver_scratch *sc)
for (di = 0; di < sc->dc; di++) for (di = 0; di < sc->dc; di++)
if (deduce_domino_single_placement(sc, di)) if (deduce_domino_single_placement(sc, di))
done_something = true; done_something = true;
if (done_something) if (done_something) {
sc->max_diff_used = max(sc->max_diff_used, DIFF_TRIVIAL);
continue; continue;
}
for (si = 0; si < sc->wh; si++) for (si = 0; si < sc->wh; si++)
if (deduce_square_single_placement(sc, si)) if (deduce_square_single_placement(sc, si))
done_something = true; done_something = true;
if (done_something) if (done_something) {
sc->max_diff_used = max(sc->max_diff_used, DIFF_TRIVIAL);
continue; continue;
}
/* FIXME: Trivial difficulty ends here */ if (max_diff_allowed <= DIFF_TRIVIAL)
continue;
for (si = 0; si < sc->wh; si++) for (si = 0; si < sc->wh; si++)
if (deduce_square_single_domino(sc, si)) if (deduce_square_single_domino(sc, si))
done_something = true; done_something = true;
if (done_something) if (done_something) {
sc->max_diff_used = max(sc->max_diff_used, DIFF_BASIC);
continue; continue;
}
for (di = 0; di < sc->dc; di++) for (di = 0; di < sc->dc; di++)
if (deduce_domino_must_overlap(sc, di)) if (deduce_domino_must_overlap(sc, di))
done_something = true; done_something = true;
if (done_something) if (done_something) {
sc->max_diff_used = max(sc->max_diff_used, DIFF_BASIC);
continue; continue;
}
} while (done_something); } while (done_something);
@ -851,7 +906,7 @@ static char *new_game_desc(const game_params *params, random_state *rs,
/* Optionally flip the domino round. */ /* Optionally flip the domino round. */
int flip = -1; int flip = -1;
if (params->unique) { if (params->diff != DIFF_AMBIGUOUS) {
int t1, t2; int t1, t2;
/* /*
* If we're after a unique solution, we can do * If we're after a unique solution, we can do
@ -910,7 +965,9 @@ static char *new_game_desc(const game_params *params, random_state *rs,
} }
assert(j == k); assert(j == k);
solver_setup_grid(sc, grid2); solver_setup_grid(sc, grid2);
} while (params->unique && run_solver(sc) > 1); } while (params->diff != DIFF_AMBIGUOUS &&
(run_solver(sc, params->diff) > 1 ||
sc->max_diff_used < params->diff));
solver_free_scratch(sc); solver_free_scratch(sc);
@ -1200,7 +1257,7 @@ static char *solve_game(const game_state *state, const game_state *currstate,
} else { } else {
struct solver_scratch *sc = solver_make_scratch(n); struct solver_scratch *sc = solver_make_scratch(n);
solver_setup_grid(sc, state->numbers->numbers); solver_setup_grid(sc, state->numbers->numbers);
run_solver(sc); run_solver(sc, DIFFCOUNT);
ret = solution_move_string(sc); ret = solution_move_string(sc);
solver_free_scratch(sc); solver_free_scratch(sc);
} }
@ -2018,7 +2075,7 @@ int main(int argc, char **argv)
game_state *s, *s2; game_state *s, *s2;
char *id = NULL, *desc; char *id = NULL, *desc;
const char *err; const char *err;
bool diagnostics = false; bool grade = false, diagnostics = false;
struct solver_scratch *sc; struct solver_scratch *sc;
int retd; int retd;
@ -2026,6 +2083,8 @@ int main(int argc, char **argv)
char *p = *++argv; char *p = *++argv;
if (!strcmp(p, "-v")) { if (!strcmp(p, "-v")) {
diagnostics = true; diagnostics = true;
} else if (!strcmp(p, "-g")) {
grade = true;
} else if (*p == '-') { } else if (*p == '-') {
fprintf(stderr, "%s: unrecognised option `%s'\n", argv[0], p); fprintf(stderr, "%s: unrecognised option `%s'\n", argv[0], p);
return 1; return 1;
@ -2035,7 +2094,7 @@ int main(int argc, char **argv)
} }
if (!id) { if (!id) {
fprintf(stderr, "usage: %s [-v] <game_id>\n", argv[0]); fprintf(stderr, "usage: %s [-v | -g] <game_id>\n", argv[0]);
return 1; return 1;
} }
@ -2058,9 +2117,12 @@ int main(int argc, char **argv)
solver_diagnostics = diagnostics; solver_diagnostics = diagnostics;
sc = solver_make_scratch(p->n); sc = solver_make_scratch(p->n);
solver_setup_grid(sc, s->numbers->numbers); solver_setup_grid(sc, s->numbers->numbers);
retd = run_solver(sc); retd = run_solver(sc, DIFFCOUNT);
if (retd == 0) { if (retd == 0) {
printf("Puzzle is inconsistent\n"); printf("Puzzle is inconsistent\n");
} else if (grade) {
printf("Difficulty rating: %s\n",
dominosa_diffnames[sc->max_diff_used]);
} else { } else {
char *move, *text; char *move, *text;
move = solution_move_string(sc); move = solution_move_string(sc);