From 1a3e746c2edcf52dc9f7126d4ea4bf1514618b2b Mon Sep 17 00:00:00 2001 From: Allen Webster Date: Wed, 19 Feb 2025 14:52:40 -0800 Subject: [PATCH] first commit --- boggler.c | 497 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ build.bat | 9 + 2 files changed, 506 insertions(+) create mode 100644 boggler.c create mode 100644 build.bat diff --git a/boggler.c b/boggler.c new file mode 100644 index 0000000..22ee688 --- /dev/null +++ b/boggler.c @@ -0,0 +1,497 @@ +/* + +A boggle solver, +by Allen Webster + +03.07.2016 (dd.mm.yyyy) + +How to use: +Call the application with two text files. + +The first should be a dictionary containing one +word per line. This system will check to make +sure the dictionary is sorted, but if it isn't +the sorting is not particularly good right now. +The dictionary is not provided but they are not +hard to find with a google search. + +The second text file is the boggle board. It should +be a 4 by 4 matrix of capital letters with no spaces +and newlines only at the end of each row. Here is +a particularly interesting board I rolled for testing: + +EYRL +OQOS +VWET +LYUS + +TODO(allen): +Better sorting +Add break down commands for querying: +-All the words of a particular length +-The number of words that start on each grid position +-The number of words that use each grid position +-The number of ways to form each word +Options for: +-Output sorted by length +-Output sorted alphabetically +*/ + +// TOP + +#include +#include +#include +#include + +struct Data{ + char *data; + int32_t size; +}; + +static struct Data +file_dump(char *filename){ + struct Data data = {0}; + FILE *file = fopen(filename, "rb"); + + if (file){ + fseek(file, 0, SEEK_END); + data.size = ftell(file); + fseek(file, 0, SEEK_SET); + + data.data = (char*)malloc(data.size+1); + fread(data.data, 1, data.size, file); + data.data[data.size] = 0; + + fclose(file); + } + + return(data); +} + +#define BOGGLE_BOARD_W 4 +#define BOGGLE_BOARD_H 4 +#define BOGGLE_BOARD_SIZE BOGGLE_BOARD_W*BOGGLE_BOARD_H + +struct Boggle_Board{ + char letter[BOGGLE_BOARD_SIZE]; +}; + +static int32_t +is_letter(char c){ + int32_t result = 0; + if ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z')){ + result = 1; + } + return(result); +} + +static char +to_upper(char c){ + if (c >= 'a' && c <= 'z'){ + c += 'A' - 'a'; + } + return(c); +} + +struct Integer_Pos{ + int32_t x, y; +}; + +#define BPOS(x,y) {x, y} + +static struct Integer_Pos moore[] = { + BPOS(-1, -1), + BPOS( 0, -1), + BPOS( 1, -1), + BPOS( 1, 0), + BPOS( 1, 1), + BPOS( 0, 1), + BPOS(-1, 1), + BPOS(-1, 0) +}; + +static struct Integer_Pos +pos_add(struct Integer_Pos a, struct Integer_Pos b){ + struct Integer_Pos r = {0}; + r.x = a.x + b.x; + r.y = a.y + b.y; + return(r); +} + +static int32_t +pos_i(struct Integer_Pos p){ + int32_t i = (p.x + p.y*BOGGLE_BOARD_W); + return(i); +} + +struct Visit_Record{ + int32_t visit[BOGGLE_BOARD_W * BOGGLE_BOARD_H]; +}; + +static struct Visit_Record +make_visit_record(){ + struct Visit_Record visit_record = {0}; + return(visit_record); +} + +struct Dictionary{ + char **word; + int32_t *len; + int32_t *already_used; + int32_t count; +}; + +static struct Dictionary +make_dictionary(int32_t word_count){ + struct Dictionary dict = {0}; + int32_t mem_size = (word_count*(sizeof(char*) + sizeof(int)*2)); + dict.word = (char**)malloc(mem_size); + dict.len = (int*)(dict.word + word_count); + dict.already_used = (int*)(dict.len + word_count); + memset(dict.word, 0, mem_size); + dict.count = 0; + return(dict); +} + +#define Swap(t,a,b) do { t T = a; a = b; b = T; } while (0) + +static void +bubble_sort(struct Dictionary dict){ + int32_t start = 0; + int32_t end = dict.count-1; + int32_t last_swap = -1; + do{ + last_swap = -1; + for (int32_t i = start; + i < end; + ++i){ + if (strcmp(dict.word[i], dict.word[i+1]) > 0){ + Swap(int32_t, dict.len[i], dict.len[i+1]); + Swap(char*, dict.word[i], dict.word[i+1]); + last_swap = i; + } + } + end = last_swap; + }while(last_swap != -1); +} + +struct Match_Range{ + int32_t first; + int32_t last; + int32_t index; +}; + +struct Match_Range +make_range(struct Dictionary dict){ + struct Match_Range range; + range.first = 0; + range.last = dict.count; + range.index = 0; + return(range); +} + +struct Match_Range +narrow_range(struct Match_Range range, + struct Dictionary dict, + char letter){ + int32_t str_index = range.index; + + int32_t s = range.first; + int32_t e = range.last; + + int32_t new_s = range.first; + int32_t new_e = range.last; + int32_t i = 0; + + if (s != e){ + for(;s < e;){ + i = (s+e)/2; + + if (i > range.first){ + if (dict.len[i-1] > str_index){ + if (dict.word[i-1][str_index] >= letter){ + e = i; + continue; + } + } + } + + if (dict.len[i] > str_index){ + if (dict.word[i][str_index] >= letter){ + break; + } + } + + s = i+1; + continue; + } + + new_s = i; + + if (s >= e){ + new_e = new_s; + } + else{ + s = new_s; + e = range.last + 1; + + for(;s < e;){ + i = (s+e)/2; + + if (i > range.first){ + if (dict.len[i-1] > str_index){ + if (dict.word[i-1][str_index] > letter){ + e = i; + continue; + } + } + } + + if (dict.len[i] > str_index){ + if (dict.word[i][str_index] > letter){ + break; + } + } + + s = i+1; + continue; + } + + new_e = i; + } + } + + struct Match_Range new_range; + new_range.first = new_s; + new_range.last = new_e; + new_range.index = str_index + 1; + return(new_range); +} + +// NOTE(allen): Boggle has no 'Q' but it has a die with a 'Qu'. +static struct Match_Range +narrow_range_boggle(struct Match_Range range, + struct Dictionary dict, + char letter){ + struct Match_Range new_range = narrow_range(range, dict, letter); + if (letter == 'Q'){ + new_range = narrow_range(new_range, dict, 'U'); + } + return(new_range); +} + +static int32_t +can_visit_pos(struct Visit_Record *visit_record, struct Integer_Pos pos){ + int32_t result = 0; + if (pos.x >= 0 && pos.x < BOGGLE_BOARD_W && + pos.y >= 0 && pos.y < BOGGLE_BOARD_H && + visit_record->visit[pos_i(pos)] == 0){ + result = 1; + } + return(result); +} + +struct Solution{ + char **words; + int32_t word_count; + int32_t total_score; +}; + +static void +explore_neighborhood(struct Dictionary dict, struct Match_Range range, + struct Boggle_Board board, struct Visit_Record *visit_record, + struct Integer_Pos pos, struct Solution *solution){ + if (dict.len[range.first] == range.index){ + if (dict.already_used[range.first] == 0){ + dict.already_used[range.first] = 1; + + solution->words[solution->word_count++] = dict.word[range.first]; + + int32_t score = 0; + if (range.index == 3){ + score = 1; + } + else if (range.index > 3){ + score = range.index - 3; + } + + solution->total_score += score; + } + } + + int32_t count = (sizeof(moore)/sizeof(*moore)); + for (int32_t i = 0; + i < count; + ++i){ + struct Integer_Pos relative_pos = pos_add(pos, moore[i]); + if (can_visit_pos(visit_record, relative_pos)){ + char letter = board.letter[pos_i(relative_pos)]; + + struct Match_Range new_range = narrow_range_boggle(range, dict, letter); + + if (new_range.first < new_range.last){ + visit_record->visit[pos_i(relative_pos)] = 1; + explore_neighborhood(dict, new_range, board, + visit_record, relative_pos, solution); + visit_record->visit[pos_i(relative_pos)] = 0; + } + } + } +} + + +int main(int argc, char **argv){ + if (argc != 3){ + fprintf(stderr, "usaged:\n\t%s \n", argv[0]); + exit(1); + } + + struct Data dictionary_file = file_dump(argv[1]); + struct Data board_file = file_dump(argv[2]); + + struct Boggle_Board board = {0}; + int32_t board_error = 0; + + { + int32_t row = 0; + int32_t col = 0; + for (int32_t i = 0; + i < board_file.size; + ++i){ + char c = board_file.data[i]; + if (is_letter(c)){ + if (col < 4){ + if (row < 4){ + board.letter[col + row*BOGGLE_BOARD_W] = to_upper(c); + ++col; + } + else{ + board_error = 1; + break; + } + } + else{ + board_error = 1; + break; + } + } + else if (c == '\r'){ + // NOTE(allen): do nothing + } + else if (c == '\n'){ + if (col == 4){ + col = 0; + ++row; + } + else{ + board_error = 1; + break; + } + } + else{ + board_error = 1; + break; + } + } + } + + if (board_error){ + fprintf(stderr, "error in boggle board format\n"); + exit(1); + } + + int32_t line_count = 1; + + for (int32_t i = 0; + i < dictionary_file.size; + ++i){ + if (dictionary_file.data[i] == '\n'){ + ++line_count; + } + } + + struct Dictionary dict = make_dictionary(line_count); + + for (int32_t i = 0; + i < dictionary_file.size; + ++i){ + + int32_t word_start = i; + + for (; i < dictionary_file.size; + ++i){ + if (dictionary_file.data[i] == '\n'){ + break; + } + } + + int32_t word_end = i; + + int32_t early_end = word_end; + int32_t good_word = 1; + for (int32_t j = word_start; + j < word_end; + ++j){ + char c = dictionary_file.data[j]; + + if (is_letter(c)){ + if (early_end != word_end){ + good_word = 0; + break; + } + dictionary_file.data[j] = to_upper(c); + } + else{ + early_end = j; + } + } + + if (good_word && (early_end - word_start) > 2){ + dictionary_file.data[early_end] = 0; + dict.len[dict.count] = (early_end - word_start); + dict.word[dict.count] = dictionary_file.data + word_start; + ++dict.count; + } + } + + bubble_sort(dict); + + struct Solution solution = {0}; + solution.words = (char**)malloc(sizeof(char*)*dict.count); + + struct Visit_Record visit_record = make_visit_record(); + struct Integer_Pos pos = {0}; + + for (pos.y = 0; + pos.y < BOGGLE_BOARD_H; + ++pos.y){ + for (pos.x = 0; + pos.x < BOGGLE_BOARD_H; + ++pos.x){ + + struct Match_Range range = make_range(dict); + + visit_record.visit[pos_i(pos)] = 1; + range = narrow_range_boggle(range, dict, board.letter[pos_i(pos)]); + explore_neighborhood(dict, range, board, &visit_record, pos, &solution); + visit_record.visit[pos_i(pos)] = 0; + } + } + + for (int32_t i = 0; + i < solution.word_count; + ++i){ + printf("%s\n", solution.words[i]); + } + printf("word count: %d\n", solution.word_count); + printf("total score: %d\n", solution.total_score); + + return(0); +} + +// BOTTOM + + + diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..fccc83f --- /dev/null +++ b/build.bat @@ -0,0 +1,9 @@ +@echo off + +set WARNINGOPS=/W4 /wd4310 /wd4100 /wd4201 /wd4505 /wd4996 /wd4127 /wd4510 /wd4512 /wd4610 /wd4390 /WX +set WARNINGOPS=%WARNINGOPS% /GR- /EHa- /nologo /FC +set WIN_LIBS=user32.lib winmm.lib gdi32.lib + +pushd w:\boggler\build +cl %WARNINGOPS% ..\code\boggler.c /Feboggler /Zi %* +popd \ No newline at end of file