Plain Sudoku, wrapped in a dungeon crawler.
Each floor is a freshly generated Sudoku puzzle. Solving it takes you one floor deeper. But every wrong digit springs a trap — and some blank cells hide loot.
Three parallel 9×9 grids hold everything the game needs.
Four systems that turn an ordinary Sudoku solver into a game.
Build a complete, valid solution first — then shuffle it four different ways, each one provably rule-preserving, so every floor looks brand new.
We never solve a blank grid from scratch. Instead we start from a guaranteed-valid board and disguise it. Each stage below keeps the Sudoku rules intact — so the result is always solvable.
void generate_map() {
int shift[9] = {0,3,6,1,4,7,2,5,8};
int num_map[10];
// 1 · build + shuffle a random 1-9 label table
for (int i = 1; i <= 9; i++) num_map[i] = i;
// ... then randomly swap entries of num_map ...
// 2 · stamp the base grid, then apply the labels
for (int r = 0; r < 9; r++)
for (int c = 0; c < 9; c++)
solution[r][c] = num_map[(c + shift[r]) % 9 + 1];
// 3 · safe swaps that keep the rules
shuffle_solution(solution);
}
Each cell is filled by one formula. Row by row, the starting number slides along a fixed offset table — so the grid fills diagonally.
int shift[9] = {0,3,6,1,4,7,2,5,8};
val = (c + shift[r]) % 9 + 1;
// row 0 → 1 2 3 4 5 6 7 8 9
// row 1 → 4 5 6 7 8 9 1 2 3 (rotated by 3)
The shift grid always looks similar. So we build a random num_map[1..9] and rename every digit through it — pure renaming, positions untouched.
for (int i = 1; i <= 9; i++) num_map[i] = i;
for (int i = 1; i <= 9; i++) {
int r = rand() % 9 + 1;
swap_int(&num_map[i], &num_map[r]);
}
// every 1 becomes (say) 7, every 2 becomes 4 ...
solution[r][c] = num_map[val];
swap_cols() exchanges two columns — but only within the same 3-column stack. Swapping column 1 ↔ column 3 reorders cells inside each box without crossing a box boundary.
void swap_cols(int g[9][9], int a, int b) {
for (int r = 0; r < 9; r++)
swap_int(&g[r][a], &g[r][b]);
}
// safe ONLY when a, b share a stack:
// {0,1,2} {3,4,5} {6,7,8}
A bigger move: swap two entire 3-row bands. Rows 1–3 trade places with rows 7–9 as whole blocks, so each 3×3 box travels intact.
void swap_row_bands(int g[9][9], int a, int b) {
for (int i = 0; i < 3; i++)
swap_rows(g, a*3 + i, b*3 + i);
}
// moves a full {row,row,row} block as one unit
shuffle_once() applies one rule-preserving swap, chosen at random from four kinds. shuffle_solution() simply runs it 200 times — compounding into a board that looks nothing like the base pattern, yet is still perfectly valid.
void shuffle_solution(int g[9][9]) {
for (int i = 0; i < 200; i++) {
shuffle_once(g);
}
}
// shuffle_once() picks one of four rule-preserving
// moves — rows, cols, bands or stacks — at random.
A good puzzle has exactly one answer. Before we hand a board to the player, we prove it can be solved — and that it can be solved only one way.
Erasing cells at random can quietly create a puzzle with two valid answers. The player guesses, gets it "wrong," and loses HP unfairly.
Every erased cell is validated against this rule before it becomes a real blank.
The atom of every check: can this digit legally go here? Scan three regions for a duplicate — the row, the column, and the 3×3 box.
for (int i = 0; i < 9; i++) {
if (g[row][i] == num) return 0; // row
if (g[i][col] == num) return 0; // column
}
int br = (row/3)*3, bc = (col/3)*3; // box origin
for (int r = br; r < br+3; r++)
for (int c = bc; c < bc+3; c++)
if (g[r][c] == num) return 0; // 3x3 box
return 1;
Scan the grid in reading order and return the first cell still equal to 0. The position is handed back through pointers — the solver always works on the next empty square.
int find_empty_cell(int g[9][9], int *row, int *col) {
for (int r = 0; r < 9; r++)
for (int c = 0; c < 9; c++)
if (g[r][c] == 0) {
*row = r; *col = c;
return 1; // found a blank
}
return 0; // grid is full
}
Find a blank, try every legal digit, and recurse. Each placement is temporary — put a number in, explore, then take it back out and try the next. Full grids count as one solution found.
if (!find_empty_cell(g, &row, &col)) return 1;
for (int num = 1; num <= 9; num++)
if (can_place_number(g, row, col, num)) {
g[row][col] = num; // place
count += count_solutions(g, limit);
g[row][col] = 0; // undo
}
place → recurse → undo · the board is restored after every branch
Counting all solutions of a sparse grid is expensive. But we don't need the total — we only need to know if it's more than one. So we stop the moment we hit a second answer.
count += count_solutions(g, limit);
g[row][col] = 0;
if (count >= limit) // limit == 2
return count; // "not unique" — stop now
Finding a 2nd solution is proof enough that the puzzle isn't unique.
We prune the entire remaining search tree the instant count reaches 2 — turning a potentially huge count into a quick yes/no answer.
The public verdict. It copies the board first — because the solver scribbles digits in and out as it works — then asks: does this puzzle have exactly one solution?
int has_unique_solution(int original[9][9]) {
int copy[9][9];
for (int r = 0; r < 9; r++)
for (int c = 0; c < 9; c++)
copy[r][c] = original[r][c];
return count_solutions(copy, 2) == 1;
}
Inside prepare_floor(), every cell we try to erase must pass the uniqueness test — or it gets put right back.
board[r][c] = 0;
if (has_unique_solution(board)) removed++; // commit
else board[r][c] = backup; // revert
A solvable puzzle is only the board. The game lives in what wraps it — depth, danger, loot, and a reason to keep descending.
One loop runs the whole game: print the board, read three numbers, act. The same r c v input doubles as commands — special triples trigger items or quit.
while (hp > 0) {
print_game();
scanf("%d %d %d", &r, &c, &v);
// r c v = 0 0 0 quit · 0 0 1 potion · 0 0 2 hint
// ... each handled inline, then `continue` ...
if (solution[r][c] == v) { // correct
board[r][c] = v; blanks--;
} else {
hp--; // wrong → trap
}
}
Deeper floors hand you emptier boards. The blank count grows with depth — then caps, so it never becomes impossible.
blanks = 25 + (floor_level * 2);
if (blanks > 60) blanks = 60; // hard ceiling
The moment a blank is committed, we roll for loot. A single rand() % 100 decides what — if anything — hides beneath it, recorded in the items grid.
int roll = rand() % 100;
if (roll < 15) items[r][c] = 1; // potion
else if (roll < 30) items[r][c] = 2; // hint
// else: nothing hidden here
probability per blank
Reveal the right digit and the loot is yours — turning a plain solve into a bit of exploration.
Every wrong digit is a trap: hp--. The loop condition is the fail state — when HP hits zero, while (hp > 0) ends and the run is over.
hp 3 / max_hp 5
if (solution[r][c] == v) {
board[r][c] = v; blanks--; // correct
} else {
hp--; // trap sprung
}
// ... loop ends the moment hp hits 0
if (hp <= 0) { /* print defeat + final floor */ }
Spend a hint and the game picks a random blank, then fills the truth straight from solution. If loot was hiding there, you collect it too — a free reveal and a possible reward.
do {
hr = rand() % 9; hc = rand() % 9;
} while (board[hr][hc] != 0); // find a blank
board[hr][hc] = solution[hr][hc]; // reveal truth
blanks--;
if (items[hr][hc] == 1) potions++; // grab loot
if (items[hr][hc] == 2) hints++;
The data is a flat 9×9 of ints, but the screen needs structure. print_game() draws 0 as ·, and prints a | every 3 columns and a divider every 3 rows — so the 3×3 boxes read clearly in plain text.
if (board[r][c] == 0) printf(". ");
else printf("%d ", board[r][c]);
if ((c+1) % 3 == 0) printf("| "); // box wall
// ...
if ((r+1) % 3 == 0) printf("+---+---+---+");