ref: 14db5e0145e0942e5cf851d696aebd2347418087
dir: /mosaic.c/
/* * mosaic.c: A puzzle based on a square grid, with some of the tiles * having clues as to how many black squares are around them. * the purpose of the game is to find what should be on all tiles (black or * unmarked) * * The game is also known as: ArtMosaico, Count and Darken, Cuenta Y Sombrea, * Fill-a-Pix, Fill-In, Komsu Karala, Magipic, Majipiku, Mosaico, Mosaik, * Mozaiek, Nampre Puzzle, Nurie-Puzzle, Oekaki-Pix, Voisimage. * * Implementation is loosely based on https://github.com/mordechaim/Mosaic, UI * interaction is based on the range puzzle in the collection. */ #include <assert.h> #include <ctype.h> #ifdef NO_TGMATH_H # include <math.h> #else # include <tgmath.h> #endif #include <stdio.h> #include <stdlib.h> #include <string.h> #include "puzzles.h" #define DEFAULT_SIZE 10 #define DEFAULT_AGGRESSIVENESS true #define MAX_TILES 10000 #define MAX_TILES_ERROR "Maximum size is 10000 tiles" #define DEFAULT_TILE_SIZE 32 #define DEBUG_IMAGE 1 #undef DEBUG_IMAGE #define FLASH_TIME 0.5F /* To enable debug prints define DEBUG_PRINTS */ /* Getting the coordinates and returning NULL when out of scope * The parentheses are needed to avoid order of operations issues */ #define get_coords(params, array, x, y) \ (((x) >= 0 && (y) >= 0) && ((x) < params->width && (y) < params->height)) \ ? array + ((y)*params->width) + x \ : NULL #define COORD_FROM_CELL(d) ((d * ds->tilesize) + ds->tilesize / 2) - 1 enum { COL_BACKGROUND = 0, COL_UNMARKED, COL_GRID, COL_MARKED, COL_BLANK, COL_TEXT_SOLVED, COL_ERROR, COL_CURSOR, NCOLOURS, COL_TEXT_DARK = COL_MARKED, COL_TEXT_LIGHT = COL_BLANK }; enum cell_state { STATE_UNMARKED = 0, STATE_MARKED = 1, STATE_BLANK = 2, STATE_SOLVED = 4, STATE_ERROR = 8, STATE_UNMARKED_ERROR = STATE_ERROR | STATE_UNMARKED, STATE_MARKED_ERROR = STATE_ERROR | STATE_MARKED, STATE_BLANK_ERROR = STATE_ERROR | STATE_BLANK, STATE_BLANK_SOLVED = STATE_SOLVED | STATE_BLANK, STATE_MARKED_SOLVED = STATE_MARKED | STATE_SOLVED, STATE_OK_NUM = STATE_BLANK | STATE_MARKED }; struct game_params { int width; int height; bool aggressive; }; typedef struct board_state board_state; typedef struct needed_list_item needed_list_item; struct needed_list_item { int x, y; needed_list_item *next; }; struct game_state { bool cheating; int not_completed_clues; int width; int height; char *cells_contents; board_state *board; }; struct board_state { unsigned int references; struct board_cell *actual_board; }; struct board_cell { signed char clue; bool shown; }; struct solution_cell { signed char cell; bool solved; bool needed; }; struct desc_cell { char clue; bool shown; bool value; bool full; bool empty; }; struct game_ui { bool solved; bool in_progress; int last_x, last_y, last_state; int cur_x, cur_y; int prev_cur_x, prev_cur_y; bool cur_visible; }; struct game_drawstate { int tilesize; int *state; int cur_x, cur_y; /* -1, -1 for no cursor displayed. */ int prev_cur_x, prev_cur_y; }; static game_params *default_params(void) { game_params *ret = snew(game_params); ret->width = DEFAULT_SIZE; ret->height = DEFAULT_SIZE; ret->aggressive = DEFAULT_AGGRESSIVENESS; return ret; } static bool game_fetch_preset(int i, char **name, game_params **params) { const int sizes[6] = { 3, 5, 10, 15, 25, 50 }; const bool aggressiveness[6] = { true, true, true, true, true, false }; if (i < 0 || i > 5) { return false; } game_params *res = snew(game_params); res->height = sizes[i]; res->width = sizes[i]; res->aggressive = aggressiveness[i]; *params = res; char value[80]; sprintf(value, "Size: %dx%d", sizes[i], sizes[i]); *name = dupstr(value); 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 *params, char const *string) { params->width = params->height = atoi(string); while (*string && isdigit((unsigned char)*string)) string++; if (*string == 'x') { string++; params->height = atoi(string); while (*string && isdigit((unsigned char)*string)) string++; } if (*string == 'h') { string++; params->aggressive = atoi(string); while (*string && isdigit((unsigned char)*string)) string++; } } static char *encode_params(const game_params *params, bool full) { char encoded[128]; int pos = 0; pos += sprintf(encoded + pos, "%dx%d", params->width, params->height); if (full) { if (params->aggressive != DEFAULT_AGGRESSIVENESS) pos += sprintf(encoded + pos, "h%d", params->aggressive); } return dupstr(encoded); } static config_item *game_configure(const game_params *params) { config_item *config = snewn(4, config_item); char value[80]; config[0].type = C_STRING; config[0].name = "Height"; sprintf(value, "%d", params->height); config[0].u.string.sval = dupstr(value); config[1].type = C_STRING; config[1].name = "Width"; sprintf(value, "%d", params->width); config[1].u.string.sval = dupstr(value); config[2].name = "Aggressive generation (longer)"; config[2].type = C_BOOLEAN; config[2].u.boolean.bval = params->aggressive; config[3].type = C_END; return config; } static game_params *custom_params(const config_item *cfg) { game_params *res = snew(game_params); res->height = atol(cfg[0].u.string.sval); res->width = atol(cfg[1].u.string.sval); res->aggressive = cfg[2].u.boolean.bval; return res; } static const char *validate_params(const game_params *params, bool full) { if (params->height < 3 || params->width < 3) { return "Minimal size is 3x3"; } if (params->height > MAX_TILES / params->width) { return MAX_TILES_ERROR; } return NULL; } static bool get_pixel(const game_params *params, const bool *image, const int x, const int y) { const bool *pixel; pixel = get_coords(params, image, x, y); if (pixel) { return *pixel; } return 0; } static void populate_cell(const game_params *params, const bool *image, const int x, const int y, bool edge, struct desc_cell *desc) { int clue = 0; bool xEdge = false; bool yEdge = false; if (edge) { if (x > 0) { clue += get_pixel(params, image, x - 1, y); if (y > 0) { clue += get_pixel(params, image, x - 1, y - 1); } if (y < params->height - 1) { clue += get_pixel(params, image, x - 1, y + 1); } } else { xEdge = true; } if (y > 0) { clue += get_pixel(params, image, x, y - 1); } else { yEdge = true; } if (x < params->width - 1) { clue += get_pixel(params, image, x + 1, y); if (y > 0) { clue += get_pixel(params, image, x + 1, y - 1); } if (y < params->height - 1) { clue += get_pixel(params, image, x + 1, y + 1); } } else { xEdge = true; } if (y < params->height - 1) { clue += get_pixel(params, image, x, y + 1); } else { yEdge = true; } } else { clue += get_pixel(params, image, x - 1, y - 1); clue += get_pixel(params, image, x - 1, y); clue += get_pixel(params, image, x - 1, y + 1); clue += get_pixel(params, image, x, y - 1); clue += get_pixel(params, image, x, y + 1); clue += get_pixel(params, image, x + 1, y - 1); clue += get_pixel(params, image, x + 1, y); clue += get_pixel(params, image, x + 1, y + 1); } desc->value = get_pixel(params, image, x, y); clue += desc->value; if (clue == 0) { desc->empty = true; desc->full = false; } else { desc->empty = false; /* setting the default */ desc->full = false; if (clue == 9) { desc->full = true; } else if (edge && ((xEdge && yEdge && clue == 4) || ((xEdge || yEdge) && clue == 6))) { desc->full = true; } } desc->shown = true; desc->clue = clue; } static void count_around(const game_params *params, struct solution_cell *sol, int x, int y, int *marked, int *blank, int *total) { int i, j; struct solution_cell *curr = NULL; (*total) = 0; (*blank) = 0; (*marked) = 0; for (i = -1; i < 2; i++) { for (j = -1; j < 2; j++) { curr = get_coords(params, sol, x + i, y + j); if (curr) { (*total)++; if ((curr->cell & STATE_BLANK) != 0) { (*blank)++; } else if ((curr->cell & STATE_MARKED) != 0) { (*marked)++; } } } } } static void count_around_state(const game_state *state, int x, int y, int *marked, int *blank, int *total) { int i, j; char *curr = NULL; (*total) = 0; (*blank) = 0; (*marked) = 0; for (i = -1; i < 2; i++) { for (j = -1; j < 2; j++) { curr = get_coords(state, state->cells_contents, x + i, y + j); if (curr) { (*total)++; if ((*curr & STATE_BLANK) != 0) { (*blank)++; } else if ((*curr & STATE_MARKED) != 0) { (*marked)++; } } } } } static void count_clues_around(const game_params *params, struct desc_cell *desc, int x, int y, int *clues, int *total) { int i, j; struct desc_cell *curr = NULL; (*total) = 0; (*clues) = 0; for (i = -1; i < 2; i++) { for (j = -1; j < 2; j++) { curr = get_coords(params, desc, x + i, y + j); if (curr) { (*total)++; if (curr->shown) { (*clues)++; } } } } } static void mark_around(const game_params *params, struct solution_cell *sol, int x, int y, int mark) { int i, j; struct solution_cell *curr; for (i = -1; i < 2; i++) { for (j = -1; j < 2; j++) { curr = get_coords(params, sol, x + i, y + j); if (curr) { if (curr->cell == STATE_UNMARKED) { curr->cell = mark; } } } } } static char solve_cell(const game_params *params, struct desc_cell *desc, struct board_cell *board, struct solution_cell *sol, int x, int y) { struct desc_cell curr; if (desc) { curr.shown = desc[(y * params->width) + x].shown; curr.clue = desc[(y * params->width) + x].clue; curr.full = desc[(y * params->width) + x].full; curr.empty = desc[(y * params->width) + x].empty; } else { curr.shown = board[(y * params->width) + x].shown; curr.clue = board[(y * params->width) + x].clue; curr.full = false; curr.empty = false; } int marked = 0, total = 0, blank = 0; if (sol[(y * params->width) + x].solved) { return 0; } count_around(params, sol, x, y, &marked, &blank, &total); if (curr.full && curr.shown) { sol[(y * params->width) + x].solved = true; if (marked + blank < total) { sol[(y * params->width) + x].needed = true; } mark_around(params, sol, x, y, STATE_MARKED); return 1; } if (curr.empty && curr.shown) { sol[(y * params->width) + x].solved = true; if (marked + blank < total) { sol[(y * params->width) + x].needed = true; } mark_around(params, sol, x, y, STATE_BLANK); return 1; } if (curr.shown) { if (!sol[(y * params->width) + x].solved) { if (marked == curr.clue) { sol[(y * params->width) + x].solved = true; if (total != marked + blank) { sol[(y * params->width) + x].needed = true; } mark_around(params, sol, x, y, STATE_BLANK); } else if (curr.clue == (total - blank)) { sol[(y * params->width) + x].solved = true; if (total != marked + blank) { sol[(y * params->width) + x].needed = true; } mark_around(params, sol, x, y, STATE_MARKED); } else if (total == marked + blank) { return -1; } else { return 0; } return 1; } return 0; } else if (total == marked + blank) { sol[(y * params->width) + x].solved = true; return 1; } else { return 0; } } static bool solve_check(const game_params *params, struct desc_cell *desc, random_state *rs, struct solution_cell **sol_return) { int x, y, i; int board_size = params->height * params->width; struct solution_cell *sol = snewn(board_size, struct solution_cell), *curr_sol; bool made_progress = true, error = false; int solved = 0, curr = 0, shown = 0; needed_list_item *head = NULL, *curr_needed, **needed_array; struct desc_cell *curr_desc; memset(sol, 0, board_size * sizeof(*sol)); for (y = 0; y < params->height; y++) { for (x = 0; x < params->width; x++) { curr_desc = get_coords(params, desc, x, y); if (curr_desc->shown) { curr_needed = snew(needed_list_item); curr_needed->next = head; head = curr_needed; curr_needed->x = x; curr_needed->y = y; shown++; } } } needed_array = snewn(shown, needed_list_item *); curr_needed = head; i = 0; while (curr_needed) { needed_array[i] = curr_needed; curr_needed = curr_needed->next; i++; } if (rs) { shuffle(needed_array, shown, sizeof(*needed_array), rs); } solved = 0; while (solved < shown && made_progress && !error) { made_progress = false; for (i = 0; i < shown; i++) { curr = solve_cell(params, desc, NULL, sol, needed_array[i]->x, needed_array[i]->y); if (curr < 0) { error = true; #ifdef DEBUG_PRINTS printf("error in cell x=%d, y=%d\n", needed_array[i]->x, needed_array[i]->y); #endif break; } if (curr > 0) { solved++; made_progress = true; } } } while (head) { curr_needed = head; head = curr_needed->next; sfree(curr_needed); } sfree(needed_array); solved = 0; /* verifying all the board is solved */ if (made_progress) { for (y = 0; y < params->height; y++) { for (x = 0; x < params->width; x++) { curr_sol = get_coords(params, sol, x, y); if ((curr_sol->cell & (STATE_MARKED | STATE_BLANK)) > 0) { solved++; } } } } if (sol_return) { *sol_return = sol; } else { sfree(sol); } return solved == board_size; } static bool solve_game_actual(const game_params *params, struct board_cell *desc, struct solution_cell **sol_return) { int x, y; int board_size = params->height * params->width; struct solution_cell *sol = snewn(board_size, struct solution_cell); bool made_progress = true, error = false; int solved = 0, curr = 0; memset(sol, 0, params->height * params->width * sizeof(*sol)); solved = 0; while (solved < params->height * params->width && made_progress && !error) { for (y = 0; y < params->height; y++) { for (x = 0; x < params->width; x++) { curr = solve_cell(params, NULL, desc, sol, x, y); if (curr < 0) { error = true; #ifdef DEBUG_PRINTS printf("error in cell x=%d, y=%d\n", x, y); #endif break; } if (curr > 0) { made_progress = true; } solved += curr; } } } if (sol_return) { *sol_return = sol; } else { sfree(sol); } return solved == params->height * params->width; } static void hide_clues(const game_params *params, struct desc_cell *desc, random_state *rs) { int shown, total, x, y, i; int needed = 0; struct desc_cell *curr; struct solution_cell *sol = NULL, *curr_sol = NULL; needed_list_item *head = NULL, *curr_needed, **needed_array; #ifdef DEBUG_PRINTS printf("Hiding clues\n"); #endif solve_check(params, desc, rs, &sol); for (y = 0; y < params->height; y++) { for (x = 0; x < params->width; x++) { count_clues_around(params, desc, x, y, &shown, &total); curr = get_coords(params, desc, x, y); curr_sol = get_coords(params, sol, x, y); if (curr_sol->needed && params->aggressive) { curr_needed = snew(needed_list_item); curr_needed->x = x; curr_needed->y = y; curr_needed->next = head; head = curr_needed; needed++; } else if (!curr_sol->needed) { curr->shown = false; } } } if (params->aggressive) { curr_needed = head; needed_array = snewn(needed, needed_list_item *); memset(needed_array, 0, needed * sizeof(*needed_array)); i = 0; while (curr_needed) { needed_array[i] = curr_needed; curr_needed = curr_needed->next; i++; } shuffle(needed_array, needed, sizeof(*needed_array), rs); for (i = 0; i < needed; i++) { curr_needed = needed_array[i]; curr = get_coords(params, desc, curr_needed->x, curr_needed->y); if (curr) { curr->shown = false; if (!solve_check(params, desc, NULL, NULL)) { #ifdef DEBUG_PRINTS printf("Hiding cell %d, %d not possible.\n", curr_needed->x, curr_needed->y); #endif curr->shown = true; } sfree(curr_needed); needed_array[i] = NULL; } curr_needed = NULL; } sfree(needed_array); } #ifdef DEBUG_PRINTS printf("needed %d\n", needed); #endif sfree(sol); } static bool start_point_check(size_t size, struct desc_cell *desc) { int i; for (i = 0; i < size; i++) { if (desc[i].empty || desc[i].full) { return true; } } return false; } 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->cur_visible) { *x = COORD_FROM_CELL(ui->cur_x); *y = COORD_FROM_CELL(ui->cur_y); *w = *h = ds->tilesize; } } static void generate_image(const game_params *params, random_state *rs, bool *image) { int x, y; for (y = 0; y < params->height; y++) { for (x = 0; x < params->width; x++) { image[(y * params->width) + x] = random_bits(rs, 1); } } } static char *new_game_desc(const game_params *params, random_state *rs, char **aux, bool interactive) { bool *image = snewn(params->height * params->width, bool); bool valid = false; char *desc_string = snewn((params->height * params->width) + 1, char); char *compressed_desc = snewn((params->height * params->width) + 1, char); char space_count; struct desc_cell *desc = snewn(params->height * params->width, struct desc_cell); int x, y, location_in_str; while (!valid) { generate_image(params, rs, image); #ifdef DEBUG_IMAGE image[0] = 1; image[1] = 1; image[2] = 0; image[3] = 1; image[4] = 1; image[5] = 0; image[6] = 0; image[7] = 0; image[8] = 0; #endif for (y = 0; y < params->height; y++) { for (x = 0; x < params->width; x++) { populate_cell(params, image, x, y, x * y == 0 || y == params->height - 1 || x == params->width - 1, &desc[(y * params->width) + x]); } } valid = start_point_check((params->height - 1) * (params->width - 1), desc); if (!valid) { #ifdef DEBUG_PRINTS printf("Not valid, regenerating.\n"); #endif } else { valid = solve_check(params, desc, rs, NULL); if (!valid) { #ifdef DEBUG_PRINTS printf("Couldn't solve, regenerating."); #endif } else { hide_clues(params, desc, rs); } } } location_in_str = 0; for (y = 0; y < params->height; y++) { for (x = 0; x < params->width; x++) { if (desc[(y * params->width) + x].shown) { #ifdef DEBUG_PRINTS printf("%d(%d)", desc[(y * params->width) + x].value, desc[(y * params->width) + x].clue); #endif sprintf(desc_string + location_in_str, "%d", desc[(y * params->width) + x].clue); } else { #ifdef DEBUG_PRINTS printf("%d( )", desc[(y * params->width) + x].value); #endif sprintf(desc_string + location_in_str, " "); } location_in_str += 1; } #ifdef DEBUG_PRINTS printf("\n"); #endif } location_in_str = 0; space_count = 'a' - 1; for (y = 0; y < params->height; y++) { for (x = 0; x < params->width; x++) { if (desc[(y * params->width) + x].shown) { if (space_count >= 'a') { sprintf(compressed_desc + location_in_str, "%c", space_count); location_in_str++; space_count = 'a' - 1; } sprintf(compressed_desc + location_in_str, "%d", desc[(y * params->width) + x].clue); location_in_str++; } else { if (space_count <= 'z') { space_count++; } else { sprintf(compressed_desc + location_in_str, "%c", space_count); location_in_str++; space_count = 'a' - 1; } } } } if (space_count >= 'a') { sprintf(compressed_desc + location_in_str, "%c", space_count); location_in_str++; } compressed_desc[location_in_str] = '\0'; #ifdef DEBUG_PRINTS printf("compressed_desc: %s\n", compressed_desc); #endif return compressed_desc; } static const char *validate_desc(const game_params *params, const char *desc) { int size_dest = params->height * params->width; int length; length = 0; while (*desc != '\0') { if (*desc >= 'a' && *desc <= 'z') { length += *desc - 'a'; } else if (*desc < '0' || *desc > '9') return "Invalid character in game description"; length++; desc++; } if (length != size_dest) { return "Desc size mismatch"; } return NULL; } static game_state *new_game(midend *me, const game_params *params, const char *desc) { game_state *state = snew(game_state); char *curr_desc = dupstr(desc); char *desc_base = curr_desc; int dest_loc; int spaces, total_spaces; state->cheating = false; state->not_completed_clues = 0; dest_loc = 0; state->height = params->height; state->width = params->width; state->cells_contents = snewn(params->height * params->width, char); memset(state->cells_contents, 0, params->height * params->width); state->board = snew(board_state); state->board->references = 1; state->board->actual_board = snewn(params->height * params->width, struct board_cell); while (*curr_desc != '\0') { if (*curr_desc >= '0' && *curr_desc <= '9') { state->board->actual_board[dest_loc].shown = true; state->not_completed_clues++; state->board->actual_board[dest_loc].clue = *curr_desc - '0'; } else { if (*curr_desc != ' ') { total_spaces = *curr_desc - 'a' + 1; } else { total_spaces = 1; } spaces = 0; while (spaces < total_spaces) { state->board->actual_board[dest_loc].shown = false; state->board->actual_board[dest_loc].clue = -1; spaces++; if (spaces < total_spaces) { dest_loc++; } } } curr_desc++; dest_loc++; } sfree(desc_base); return state; } static game_state *dup_game(const game_state *state) { game_state *ret = snew(game_state); ret->cheating = state->cheating; ret->not_completed_clues = state->not_completed_clues; ret->width = state->width; ret->height = state->height; ret->cells_contents = snewn(state->height * state->width, char); memcpy(ret->cells_contents, state->cells_contents, state->height * state->width); ret->board = state->board; ret->board->references++; return ret; } static void free_game(game_state *state) { sfree(state->cells_contents); state->cells_contents = NULL; if (state->board->references <= 1) { sfree(state->board->actual_board); sfree(state->board); state->board = NULL; } else { state->board->references--; } sfree(state); } static char *solve_game(const game_state *state, const game_state *currstate, const char *aux, const char **error) { struct solution_cell *sol = NULL; game_params param; bool solved; char *ret = NULL; unsigned int curr_ret; int i, bits, ret_loc = 1; int size = state->width * state->height; param.width = state->width; param.height = state->height; solved = solve_game_actual(¶m, state->board->actual_board, &sol); if (!solved) { *error = dupstr("Could not solve this board"); sfree(sol); return NULL; } ret = snewn((size / 4) + 3, char); ret[0] = 's'; i = 0; while (i < size) { curr_ret = 0; bits = 0; while (bits < 8 && i < size) { curr_ret <<= 1; curr_ret |= sol[i].cell == STATE_MARKED; i++; bits++; } curr_ret <<= 8 - bits; sprintf(ret + ret_loc, "%02x", curr_ret); ret_loc += 2; } sfree(sol); return ret; } static bool game_can_format_as_text_now(const game_params *params) { return true; } static char *game_text_format(const game_state *state) { char *desc_string = snewn((state->height * state->width) * 3 + 1, char); int location_in_str = 0, x, y; for (y = 0; y < state->height; y++) { for (x = 0; x < state->width; x++) { if (state->board->actual_board[(y * state->width) + x].shown) { sprintf(desc_string + location_in_str, "|%d|", state->board->actual_board[(y * state->width) + x].clue); } else { sprintf(desc_string + location_in_str, "| |"); } location_in_str += 3; } sprintf(desc_string + location_in_str, "\n"); location_in_str += 1; } return desc_string; } static game_ui *new_ui(const game_state *state) { game_ui *ui = snew(game_ui); ui->last_x = -1; ui->last_y = -1; ui->last_state = 0; ui->solved = false; ui->cur_x = ui->cur_y = 0; ui->cur_visible = getenv_bool("PUZZLES_SHOW_CURSOR", false); return ui; } static void free_ui(game_ui *ui) { sfree(ui); } 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) { char *cell; if (IS_CURSOR_SELECT(button)) { if (!ui->cur_visible || state->not_completed_clues == 0) return ""; cell = get_coords(state, state->cells_contents, ui->cur_x, ui->cur_y); switch (*cell & STATE_OK_NUM) { case STATE_UNMARKED: return button == CURSOR_SELECT ? "Black" : "White"; case STATE_MARKED: return button == CURSOR_SELECT ? "White" : "Empty"; case STATE_BLANK: return button == CURSOR_SELECT ? "Empty" : "Black"; } } return ""; } static char *interpret_move(const game_state *state, game_ui *ui, const game_drawstate *ds, int x, int y, int button) { int srcX = ui->last_x, srcY = ui->last_y; int offsetX, offsetY, gameX, gameY, i; int dirX, dirY, diff; char move_type; char move_desc[80]; char *ret = NULL; const char *cell_state; bool changed = false; if (state->not_completed_clues == 0 && !IS_CURSOR_MOVE(button)) { return NULL; } offsetX = x - (ds->tilesize / 2); offsetY = y - (ds->tilesize / 2); gameX = offsetX / ds->tilesize; gameY = offsetY / ds->tilesize; if ((IS_MOUSE_DOWN(button) || IS_MOUSE_DRAG(button) || IS_MOUSE_RELEASE(button)) && ((offsetX < 0) || (offsetY < 0))) return NULL; if (button == LEFT_BUTTON || button == RIGHT_BUTTON) { cell_state = get_coords(state, state->cells_contents, gameX, gameY); if (cell_state) { ui->last_state = *cell_state & (STATE_BLANK | STATE_MARKED); ui->last_state = (ui->last_state + ((button == RIGHT_BUTTON) ? 2 : 1)) % (STATE_BLANK | STATE_MARKED); } if (button == RIGHT_BUTTON) { /* Right button toggles twice */ move_type = 'T'; } else { move_type = 't'; } if (gameX >= 0 && gameY >= 0 && gameX < state->width && gameY < state->height) { sprintf(move_desc, "%c%d,%d", move_type, gameX, gameY); ui->last_x = gameX; ui->last_y = gameY; ret = dupstr(move_desc); } else { ui->last_x = -1; ui->last_y = -1; } changed = true; ui->cur_visible = false; } else if (button == LEFT_DRAG || button == RIGHT_DRAG) { move_type = 'd'; /* allowing only drags in straight lines */ if (gameX >= 0 && gameY >= 0 && gameX < state->width && gameY < state->height && ui->last_x >= 0 && ui->last_y >= 0 && (gameY == ui->last_y || gameX == ui->last_x)) { sprintf(move_desc, "%c%d,%d,%d,%d,%d", move_type, gameX, gameY, ui->last_x, ui->last_y, ui->last_state); if (srcX == gameX && srcY != gameY) { dirX = 0; diff = srcY - gameY; if (diff < 0) { dirY = -1; diff *= -1; } else { dirY = 1; } } else { diff = srcX - gameX; dirY = 0; if (diff < 0) { dirX = -1; diff *= -1; } else { dirX = 1; } } for (i = 0; i < diff; i++) { cell_state = get_coords(state, state->cells_contents, gameX + (dirX * i), gameY + (dirY * i)); if (cell_state && (*cell_state & STATE_OK_NUM) == 0 && ui->last_state > 0) { changed = true; break; } } ui->last_x = gameX; ui->last_y = gameY; if (changed) { ret = dupstr(move_desc); } } else { ui->last_x = -1; ui->last_y = -1; } ui->cur_visible = false; } else if (button == LEFT_RELEASE || button == RIGHT_RELEASE) { move_type = 'e'; if (gameX >= 0 && gameY >= 0 && gameX < state->width && gameY < state->height && ui->last_x >= 0 && ui->last_y >= 0 && (gameY == ui->last_y || gameX == ui->last_x)) { sprintf(move_desc, "%c%d,%d,%d,%d,%d", move_type, gameX, gameY, ui->last_x, ui->last_y, ui->last_state); if (srcX == gameX && srcY != gameY) { dirX = 0; diff = srcY - gameY; if (diff < 0) { dirY = -1; diff *= -1; } else { dirY = 1; } } else { diff = srcX - gameX; dirY = 0; if (diff < 0) { dirX = -1; diff *= -1; } else { dirX = 1; } } for (i = 0; i < diff; i++) { cell_state = get_coords(state, state->cells_contents, gameX + (dirX * i), gameY + (dirY * i)); if (cell_state && (*cell_state & STATE_OK_NUM) == 0 && ui->last_state > 0) { changed = true; break; } } if (changed) { ret = dupstr(move_desc); } } else { ui->last_x = -1; ui->last_y = -1; } ui->cur_visible = false; } else if (IS_CURSOR_MOVE(button)) { ui->prev_cur_x = ui->cur_x; ui->prev_cur_y = ui->cur_y; move_cursor(button, &ui->cur_x, &ui->cur_y, state->width, state->height, false); ui->cur_visible = true; return MOVE_UI_UPDATE; } else if (IS_CURSOR_SELECT(button)) { if (!ui->cur_visible) { ui->cur_x = 0; ui->cur_y = 0; ui->cur_visible = true; return MOVE_UI_UPDATE; } if (button == CURSOR_SELECT2) { sprintf(move_desc, "T%d,%d", ui->cur_x, ui->cur_y); ret = dupstr(move_desc); } else { /* Otherwise, treat as LEFT_BUTTON, for a single square. */ sprintf(move_desc, "t%d,%d", ui->cur_x, ui->cur_y); ret = dupstr(move_desc); } } return ret; } static void update_board_state_around(game_state *state, int x, int y) { int i, j; struct board_cell *curr; char *curr_state; int total; int blank; int marked; for (i = -1; i < 2; i++) { for (j = -1; j < 2; j++) { curr = get_coords(state, state->board->actual_board, x + i, y + j); if (curr && curr->shown) { curr_state = get_coords(state, state->cells_contents, x + i, y + j); count_around_state(state, x + i, y + j, &marked, &blank, &total); if (curr->clue == marked && (total - marked - blank) == 0) { *curr_state &= STATE_MARKED | STATE_BLANK; *curr_state |= STATE_SOLVED; } else if (curr->clue < marked || curr->clue > (total - blank)) { *curr_state &= STATE_MARKED | STATE_BLANK; *curr_state |= STATE_ERROR; } else { *curr_state &= STATE_MARKED | STATE_BLANK; } } } } } static game_state *execute_move(const game_state *state, const char *move) { game_state *new_state = dup_game(state); int i = 0, x = -1, y = -1, clues_left = 0; int srcX = -1, srcY = -1, size = state->height * state->width; const char *p; char *cell, sol_char; int steps = 1, bits, sol_location, dirX, dirY, diff, last_state = STATE_UNMARKED; unsigned int sol_value; struct board_cell *curr_cell; char move_type; int nparams = 0, move_params[5]; p = move; move_type = *p++; switch (move_type) { case 't': case 'T': nparams = 2; break; case 'd': case 'e': nparams = 5; break; } for (i = 0; i < nparams; i++) { move_params[i] = atoi(p); while (*p && isdigit((unsigned char)*p)) p++; if (i+1 < nparams) { if (*p != ',') { free_game(new_state); return NULL; } p++; } } if (move_type == 't' || move_type == 'T') { if (move_type == 'T') { steps++; } x = move_params[0]; y = move_params[1]; if (x == -1 || y == -1) { return new_state; } cell = get_coords(new_state, new_state->cells_contents, x, y); if (cell == NULL) { free_game(new_state); return NULL; } if (*cell >= STATE_OK_NUM) { *cell &= STATE_OK_NUM; } *cell = (*cell + steps) % STATE_OK_NUM; update_board_state_around(new_state, x, y); } else if (move_type == 's') { new_state->not_completed_clues = 0; new_state->cheating = true; sol_location = 0; bits = 0; i = 1; while (i < strlen(move)) { sol_value = 0; while (bits < 8) { sol_value <<= 4; sol_char = move[i]; if (sol_char >= '0' && sol_char <= '9') { sol_value |= sol_char - '0'; } else { sol_value |= (sol_char - 'a') + 10; } bits += 4; i++; } while (bits > 0 && sol_location < size) { if (sol_value & 0x80) { new_state->cells_contents[sol_location] = STATE_MARKED_SOLVED; } else { new_state->cells_contents[sol_location] = STATE_BLANK_SOLVED; } sol_value <<= 1; bits--; sol_location++; } } return new_state; } else if (move_type == 'd' || move_type == 'e') { x = move_params[0]; y = move_params[1]; srcX = move_params[2]; srcY = move_params[3]; last_state = move_params[4]; if (srcX == x && srcY != y) { dirX = 0; diff = srcY - y; if (diff < 0) { dirY = -1; diff *= -1; } else { dirY = 1; } } else { diff = srcX - x; dirY = 0; if (diff < 0) { dirX = -1; diff *= -1; } else { dirX = 1; } } for (i = 0; i < diff; i++) { cell = get_coords(new_state, new_state->cells_contents, x + (dirX * i), y + (dirY * i)); if (cell == NULL) { free_game(new_state); return NULL; } if ((*cell & STATE_OK_NUM) == 0) { *cell = last_state; update_board_state_around(new_state, x + (dirX * i), y + (dirY * i)); } } } for (y = 0; y < state->height; y++) { for (x = 0; x < state->width; x++) { cell = get_coords(new_state, new_state->cells_contents, x, y); curr_cell = get_coords(new_state, new_state->board->actual_board, x, y); if (curr_cell->shown && ((*cell & STATE_SOLVED) == 0)) { clues_left++; } } } new_state->not_completed_clues = clues_left; return new_state; } /* ---------------------------------------------------------------------- * Drawing routines. */ static void game_compute_size(const game_params *params, int tilesize, const game_ui *ui, int *x, int *y) { *x = (params->width + 1) * tilesize; *y = (params->height + 1) * tilesize; } static void game_set_size(drawing *dr, game_drawstate *ds, const game_params *params, int tilesize) { ds->tilesize = tilesize; } #define COLOUR(ret, i, r, g, b) \ ((ret[3 * (i) + 0] = (r)), (ret[3 * (i) + 1] = (g)), (ret[3 * (i) + 2] = (b))) static float *game_colours(frontend *fe, int *ncolours) { float *ret = snewn(3 * NCOLOURS, float); frontend_default_colour(fe, &ret[COL_BACKGROUND * 3]); COLOUR(ret, COL_GRID, 0.0F, 102 / 255.0F, 99 / 255.0F); COLOUR(ret, COL_ERROR, 1.0F, 0.0F, 0.0F); COLOUR(ret, COL_BLANK, 236 / 255.0F, 236 / 255.0F, 236 / 255.0F); COLOUR(ret, COL_MARKED, 20 / 255.0F, 20 / 255.0F, 20 / 255.0F); COLOUR(ret, COL_UNMARKED, 148 / 255.0F, 196 / 255.0F, 190 / 255.0F); COLOUR(ret, COL_TEXT_SOLVED, 100 / 255.0F, 100 / 255.0F, 100 / 255.0F); COLOUR(ret, COL_CURSOR, 255 / 255.0F, 200 / 255.0F, 200 / 255.0F); *ncolours = NCOLOURS; return ret; } /* Extra flags in game_drawstate entries, not in main game state */ #define DRAWFLAG_CURSOR 0x100 #define DRAWFLAG_CURSOR_U 0x200 #define DRAWFLAG_CURSOR_L 0x400 #define DRAWFLAG_CURSOR_UL 0x800 #define DRAWFLAG_MARGIN_R 0x1000 #define DRAWFLAG_MARGIN_D 0x2000 static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state) { struct game_drawstate *ds = snew(game_drawstate); int i; ds->tilesize = 0; ds->state = NULL; ds->state = snewn((state->width + 1) * (state->height + 1), int); for (i = 0; i < (state->width + 1) * (state->height + 1); i++) ds->state[i] = -1; return ds; } static void game_free_drawstate(drawing *dr, game_drawstate *ds) { sfree(ds->state); sfree(ds); } static void draw_cell(drawing *dr, int cell, int ts, signed char clue_val, int x, int y) { int startX = ((x * ts) + ts / 2) - 1, startY = ((y * ts) + ts / 2) - 1; int color, text_color = COL_TEXT_DARK; clip(dr, startX - 1, startY - 1, ts, ts); if (!(cell & DRAWFLAG_MARGIN_R)) draw_rect(dr, startX - 1, startY - 1, ts, 1, (cell & (DRAWFLAG_CURSOR | DRAWFLAG_CURSOR_U) ? COL_CURSOR : COL_GRID)); if (!(cell & DRAWFLAG_MARGIN_D)) draw_rect(dr, startX - 1, startY - 1, 1, ts, (cell & (DRAWFLAG_CURSOR | DRAWFLAG_CURSOR_L) ? COL_CURSOR : COL_GRID)); if (cell & DRAWFLAG_CURSOR_UL) draw_rect(dr, startX - 1, startY - 1, 1, 1, COL_CURSOR); if (!(cell & (DRAWFLAG_MARGIN_R | DRAWFLAG_MARGIN_D))) { if (cell & STATE_MARKED) { color = COL_MARKED; text_color = COL_TEXT_LIGHT; } else if (cell & STATE_BLANK) { text_color = COL_TEXT_DARK; color = COL_BLANK; } else { text_color = COL_TEXT_DARK; color = COL_UNMARKED; } if (cell & STATE_ERROR) { text_color = COL_ERROR; } else if (cell & STATE_SOLVED) { text_color = COL_TEXT_SOLVED; } draw_rect(dr, startX, startY, ts - 1, ts - 1, color); if (clue_val >= 0) { char clue[80]; sprintf(clue, "%d", clue_val); draw_text(dr, startX + ts / 2, startY + ts / 2, 1, ts * 3 / 5, ALIGN_VCENTRE | ALIGN_HCENTRE, text_color, clue); } } unclip(dr); draw_update(dr, startX - 1, startY - 1, ts, ts); } 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 x, y; char status[80]; signed char clue_val; bool flashing = (flashtime > 0 && (flashtime <= FLASH_TIME / 3 || flashtime > 2*FLASH_TIME / 3)); for (y = 0; y <= state->height; y++) { for (x = 0; x <= state->width; x++) { bool inbounds = x < state->width && y < state->height; int cell = (inbounds ? state->cells_contents[(y * state->width) + x] : 0); if (x == state->width) cell |= DRAWFLAG_MARGIN_R; if (y == state->height) cell |= DRAWFLAG_MARGIN_D; if (flashing) cell ^= (STATE_BLANK | STATE_MARKED); if (ui->cur_visible) { if (ui->cur_x == x && ui->cur_y == y) cell |= DRAWFLAG_CURSOR; if (ui->cur_x == x-1 && ui->cur_y == y) cell |= DRAWFLAG_CURSOR_L; if (ui->cur_x == x && ui->cur_y == y-1) cell |= DRAWFLAG_CURSOR_U; if (ui->cur_x == x-1 && ui->cur_y == y-1) cell |= DRAWFLAG_CURSOR_UL; } if (inbounds && state->board->actual_board[(y * state->width) + x].shown) { clue_val = state->board->actual_board[ (y * state->width) + x].clue; } else { clue_val = -1; } if (ds->state[(y * (state->width+1)) + x] != cell) { draw_cell(dr, cell, ds->tilesize, clue_val, x, y); ds->state[(y * (state->width+1)) + x] = cell; } } } sprintf(status, "Clues left: %d", state->not_completed_clues); if (state->not_completed_clues == 0 && !state->cheating) { sprintf(status, "COMPLETED!"); } else if (state->not_completed_clues == 0 && state->cheating) { sprintf(status, "Auto solved"); } status_bar(dr, status); } 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->cheating && oldstate->not_completed_clues > 0 && newstate->not_completed_clues == 0) { return FLASH_TIME; } return 0.0F; } static int game_status(const game_state *state) { if (state->not_completed_clues == 0) return +1; return 0; } #ifdef COMBINED #define thegame mosaic #endif const struct game thegame = { "Mosaic", "games.mosaic", "mosaic", 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, NULL, NULL, /* 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, DEFAULT_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, false, false, NULL, NULL, /* print_size, print */ true, /* wants_statusbar */ false, NULL, /* timing_state */ 0, /* flags */ };