I wrote a simple Sudoku solver

Thanks for sharing! I took a few minutes and refactored it to match how I’ve been coding Zig lately. See what you think.

I did this in the branch that I’m working on because I wanted to try out and see if the new API for dealing with delimiters is more convenient.

Some notes:

  • No allocation needed for this
  • By wrapping the state into a struct, it becomes convenient to use namespaced functions
  • unreachable => return error.Unsolvable since it can happen in response to user input. Assertions for programmer mistakes; errors for user mistakes.
//! A sudoku solver.
const std = @import("std");
const Allocator = std.mem.Allocator;

const Sudoku = struct {
    /// We are representing a sukodu puzzle as a 81 cell array.
    /// 0's are going to indicate empty cells, while 1-9 cells have been filled in.
    cells: [9 * 9]u8,

    /// Parse a sudoku puzzle from a line of ASCII/UTF-8 text.
    /// The line should contain atleast 81 numbers in the range 0-9, with 0's for empty cells.
    fn parse(slice: []const u8) !Sudoku {
        var result: Sudoku = undefined;

        if (slice.len < result.cells.len) {
            return error.TooFewBytes;
        }

        for (&result.cells, slice[0..result.cells.len]) |*cell, input| {
            // Check that all bytes lie in the range '0' - '9'
            switch (input) {
                '0'...'9' => cell.* = input - '0',
                else => return error.InvalidByte,
            }
        }

        return result;
    }

    /// Convenience function for displaying a sudoku as a 9x9 grid of digits.
    fn display(sudoku: *Sudoku, writer: *std.io.BufferedWriter) !void {
        for (sudoku.cells, 0..) |cell, i| {
            try writer.print("{d} ", .{cell});
            if (i % 9 == 0) try writer.writeByte('\n');
        }
    }

    /// Print the solution to stdout.
    /// First we need to push all the digits into valid ASCII/UTF-8 numbers.
    fn print(sudoku: *Sudoku, writer: *std.io.BufferedWriter) !void {
        for (sudoku.cells) |cell| {
            try writer.writeByte('0' + cell);
        }
        try writer.writeByte('\n');
    }

    /// Solves a sudoku in place via brute force.
    /// Simply start at the top-left and look through the puzzle for a cell
    /// where only a single digit is allowed.
    /// When found, fill in the digit and start over from the top-left,
    /// until there are no more cells to fill in.
    fn solveBruteForce(sudoku: *Sudoku) !void {
        outer: while (true) {
            for (0..9) |i| {
                square: for (0..9) |j| {
                    if (sudoku.cells[i * 9 + j] != 0) {
                        continue; // Already filled in this digit so skip.
                    }

                    // Compute the indicies of the cells "seen by" the current cell.
                    const seen = getSeenByCell(i, j);

                    // Compute the set of numbers that are valid in this cell.
                    var valid_in_slot = [_]bool{true} ** 9;
                    for (seen) |cell_idx| {
                        const val = sudoku.cells[cell_idx];
                        if (val > 0 and valid_in_slot[val - 1]) {
                            valid_in_slot[val - 1] = false;
                        }
                    }

                    // Compute which digit is valid.
                    var digit: usize = 0;
                    for (valid_in_slot, 0..) |is_valid, idx| {
                        if (is_valid) {
                            if (digit == 0) {
                                digit = idx + 1;
                            } else {
                                // More than one digit is valid, so skip this cell for now.
                                continue :square;
                            }
                        }
                    }

                    if (digit == 0) {
                        // No digit is valid in this cell, so the sudoku is unsolvable.
                        return error.Unsolvable;
                    }

                    // Put the valid digit into the square;
                    sudoku.cells[i * 9 + j] = @truncate(digit);
                    continue :outer; // We placed a digit, so we scan for a new square to fill again.
                }
            }

            // When this is reached, we looped through every cell without finding a missing one,
            // so the sudoku is solved.
            break :outer;
        }
    }

    /// Get's the indicies of the cells that "can be seen by" the cell at (i, j).
    /// Cell A is seen by cell B if A is in the same row, column or box as B.
    fn getSeenByCell(i: usize, j: usize) [9 + 9 + 9]usize {
        const bi = (i / 3) * 3;
        const bj = (j / 3) * 3;
        return [_]usize{
            // Row
            i * 9 + 0,
            i * 9 + 1,
            i * 9 + 2,
            i * 9 + 3,
            i * 9 + 4,
            i * 9 + 5,
            i * 9 + 6,
            i * 9 + 7,
            i * 9 + 8,
            // Column
            0 * 9 + j,
            1 * 9 + j,
            2 * 9 + j,
            3 * 9 + j,
            4 * 9 + j,
            5 * 9 + j,
            6 * 9 + j,
            7 * 9 + j,
            8 * 9 + j,
            // Box
            (bi + 0) * 9 + bj + 0,
            (bi + 0) * 9 + bj + 1,
            (bi + 0) * 9 + bj + 2,
            (bi + 1) * 9 + bj + 0,
            (bi + 1) * 9 + bj + 1,
            (bi + 1) * 9 + bj + 2,
            (bi + 2) * 9 + bj + 0,
            (bi + 2) * 9 + bj + 1,
            (bi + 2) * 9 + bj + 2,
        };
    }
};

const usage =
    \\Usage: zig-sudoku [path-to-puzzles]
    \\Solve sudoku puzzles. Provide a path to a file containing puzzles.
    \\Each puzzle should be formatted as a newline terminated line of digits 0-9.
    \\0 indicates an empty cell, while any other digit is a prefilled one.
    \\
;

pub fn main() !void {
    var arena_allocator = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena_allocator.deinit();
    const arena = arena_allocator.allocator();

    const args = try std.process.argsAlloc(arena);

    // Check that a command line argument was supplied.
    if (args.len != 2) {
        try std.fs.File.stderr().writeAll(usage);
        std.process.exit(1);
    }

    // Open file to read puzzles from.
    const file = try std.fs.cwd().openFile(args[1], .{});
    defer file.close();

    var file_reader = file.reader();
    var in_buffer: [512]u8 = undefined;
    var in_br = file_reader.interface().buffered(&in_buffer);

    var out_buffer: [512]u8 = undefined;
    var out_writer = std.fs.File.stdout().writerStreaming();
    var out_bw = out_writer.interface().buffered(&out_buffer);

    // Read the file line by line.
    while (in_br.takeSentinel('\n')) |line| {
        // Try to parse and solve the line as a sudoku puzzle.
        var puzzle = try Sudoku.parse(line);
        try puzzle.solveBruteForce();
        try puzzle.print(&out_bw);
    } else |err| switch (err) {
        error.EndOfStream => {},
        else => |e| return e,
    }

    try out_bw.flush();
}
10 Likes