├── .github └── workflows │ ├── go.yml │ └── pages.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── generator │ └── main.go ├── solver │ └── main.go └── wasm │ ├── Makefile │ ├── assets │ ├── index.html │ ├── wasm_exec.js │ └── worker.js │ └── main.go ├── difficulty.go ├── difficulty_test.go ├── digits.go ├── digits_test.go ├── doc └── genb.svg ├── generator.go ├── generator_test.go ├── go.mod ├── go.sum ├── inputs ├── norvig-easy50.txt ├── norvig-hard.txt └── norvig-hardlong.txt ├── sudoku.go ├── sudoku_fuzz_test.go ├── sudoku_test.go └── svg ├── svg.go └── svg_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run Go tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: 1.22.2 20 | 21 | - name: Test 22 | # Running ./... won't work since cmd/wasm needs js tag settings. 23 | run: go test -v . ./svg 24 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | 34 | - name: Set up Go 35 | uses: actions/setup-go@v4 36 | with: 37 | go-version: 1.22.2 38 | 39 | - name: Setup Pages 40 | uses: actions/configure-pages@v2 41 | 42 | - name: Build wasm 43 | run: | 44 | cd cmd/wasm 45 | make build 46 | 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v1 49 | with: 50 | # Upload 51 | path: 'cmd/wasm/assets' 52 | 53 | - name: Deploy to GitHub Pages 54 | id: deployment 55 | uses: actions/deploy-pages@v1 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.wasm 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-sudoku 2 | 3 | This is a toolkit for solving and generating [Sudoku 4 | puzzles](https://en.wikipedia.org/wiki/Sudoku) in Go. 5 | 6 | ## What's inside 7 | 8 | All the code is in a single top-level package: `sudoku`. It's broadly separated 9 | into three parts: 10 | 11 | * `sudoku.go`: board representation and functions for parsing boards from 12 | strings, emitting boards back to output and solving Sudoku puzzles. The 13 | basic solver uses constraint propagation and recursive search and is based on 14 | [Peter Norvig's old post](https://norvig.com/sudoku.html), although the Go 15 | code is about 100x faster than Norvig's Python (faster compiled language but 16 | also an optimized board representation). 17 | 18 | Contains additional functionality like finding _all_ the solutions of a given 19 | puzzle and not just a single solution. 20 | 21 | * `generator.go`: generate valid Sudoku puzzles that have a single solution. 22 | The algorithm is based on a mish-mash of information found online and tweaked 23 | by me. Contains additional functionality like generating _symmetrical_ 24 | Sudoku boards. 25 | 26 | Note: generating hard-to-solve boards with a single solution is fairly 27 | difficult. The best way to do this in practice seems to be to generate a 28 | _large_ number of boards, sifting through them to find the hardest ones. 29 | These boards can then be transformed in a myriad ways to retain the same 30 | difficulty but look and feel very different (through swapping rows and 31 | columns, rotations, and permuting the existing hint digits). Therefore, 32 | a single genuienly hard board can be replayed in many different ways. 33 | 34 | * `difficulty.go`: code to evaluate the difficulty of a given Sudoku puzzle; 35 | the approach was partially inspired by the paper "Sudoku Puzzles Generating: 36 | from Easy to Evil" by Xiang-Sun ZHANG's research group. 37 | 38 | The `cmd` directory has two command-line tools: `generator` and `solver` that 39 | demonstrate the use of the package. 40 | 41 | ## Testing 42 | 43 | Some tests take a while to run, so they are excluded if the `-short` testing 44 | flag is provided: 45 | 46 | $ go test -v -short ./... 47 | 48 | ## Generating printable boards 49 | 50 | `go-sudoku` includes some rudimentary functionality to emit a Sudoku board into 51 | a printable SVG format, like this: 52 | 53 | ![SVG board sample](/doc/genb.svg) 54 | 55 | You can invoke the `cmd/generator` command with the `-svgout` flag to see this 56 | in action, or use the web interface. 57 | 58 | ## Web interface 59 | 60 | This repository includes a web interface for generating Sudoku puzzles, by 61 | compiling the Go code to WebAssembly. To run it locally: 62 | 63 | $ cd cmd/wasm 64 | $ make build 65 | $ make serve 66 | 67 | This will run a local webserver; open http://localhost:8899 in your browser 68 | to generate puzzles! 69 | 70 | The repository also has a GitHub actions setup to automatically deploy the web 71 | interface to https://eliben.github.io/go-sudoku on each commit. 72 | -------------------------------------------------------------------------------- /cmd/generator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "os" 9 | "time" 10 | 11 | "github.com/eliben/go-sudoku" 12 | ) 13 | 14 | // Note: trying to generate difficult boards with low hintcount may take a long 15 | // time. 16 | 17 | var symFlag = flag.Bool("sym", false, "generate a symmetrical puzzle") 18 | var diffFlag = flag.Float64("diff", 2.5, "minimal difficulty for generated puzzle") 19 | var hintCountFlag = flag.Int("hintcount", 28, "hint count for generation; higher counts lead to easier puzzles") 20 | var svgOutFlag = flag.String("svgout", "", "file name for SVG output, if needed") 21 | 22 | func main() { 23 | flag.Usage = func() { 24 | out := flag.CommandLine.Output() 25 | fmt.Println(out, "usage: generator [options]") 26 | fmt.Println("Options:") 27 | flag.PrintDefaults() 28 | } 29 | flag.Parse() 30 | 31 | rand.Seed(time.Now().UnixNano()) 32 | 33 | count := 0 34 | maxDifficultySeen := 0.0 35 | 36 | for { 37 | var board sudoku.Values 38 | 39 | if *symFlag { 40 | board = sudoku.GenerateSymmetrical(*hintCountFlag) 41 | } else { 42 | board = sudoku.Generate(*hintCountFlag) 43 | } 44 | 45 | d, err := sudoku.EvaluateDifficulty(board) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | if d >= *diffFlag { 51 | fmt.Println(sudoku.DisplayAsInput(board)) 52 | fmt.Printf("Difficulty: %.2f\n", d) 53 | 54 | if len(*svgOutFlag) > 0 { 55 | f, err := os.Create(*svgOutFlag) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | defer f.Close() 60 | sudoku.DisplayAsSVG(f, board, d) 61 | fmt.Println("Wrote SVG output to", *svgOutFlag) 62 | } 63 | 64 | break 65 | } else { 66 | count++ 67 | if d > maxDifficultySeen { 68 | maxDifficultySeen = d 69 | } 70 | 71 | if count > 0 && count%10 == 0 { 72 | fmt.Printf("Tried %v boards; max difficulty seen %.2f\n", count, maxDifficultySeen) 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /cmd/solver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/eliben/go-sudoku" 14 | ) 15 | 16 | var statsFlag = flag.Bool("stats", false, "enable stats for solving") 17 | var randomizeFlag = flag.Bool("randomize", false, "randomize solving order") 18 | var actionFlag = flag.String("action", "solve", "action to perform: solve, count") 19 | 20 | func main() { 21 | flag.Usage = func() { 22 | out := flag.CommandLine.Output() 23 | fmt.Fprintln(out, "usage: solver [options] ") 24 | fmt.Fprintln(out, "Options:") 25 | flag.PrintDefaults() 26 | } 27 | flag.Parse() 28 | 29 | switch *actionFlag { 30 | case "solve": 31 | solveAndReport() 32 | case "count": 33 | countHints() 34 | default: 35 | flag.Usage() 36 | log.Fatal("Please select one of the supported actions.") 37 | } 38 | } 39 | 40 | func solveAndReport() { 41 | var totalDuration time.Duration = 0 42 | var maxDuration time.Duration = 0 43 | var totalSearches uint64 = 0 44 | var totalDifficulty float64 45 | var maxSearches uint64 = 0 46 | var numBoards int = 0 47 | var numSolved int = 0 48 | 49 | if *statsFlag { 50 | sudoku.EnableStats = true 51 | } 52 | 53 | if *randomizeFlag { 54 | rand.Seed(time.Now().UnixNano()) 55 | } 56 | 57 | boards := getInputBoards() 58 | for _, board := range boards { 59 | numBoards++ 60 | 61 | v, err := sudoku.ParseBoard(board, false) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | d, err := sudoku.EvaluateDifficulty(v) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | totalDifficulty += d 70 | 71 | tStart := time.Now() 72 | sudoku.EliminateAll(v) 73 | v, _ = sudoku.Solve(v, sudoku.SolveOptions{Randomize: *randomizeFlag}) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | tElapsed := time.Now().Sub(tStart) 78 | 79 | totalDuration += tElapsed 80 | if tElapsed > maxDuration { 81 | maxDuration = tElapsed 82 | } 83 | 84 | if sudoku.IsSolved(v) { 85 | numSolved++ 86 | } 87 | 88 | if *statsFlag { 89 | totalSearches += sudoku.Stats.NumSearches 90 | if sudoku.Stats.NumSearches > maxSearches { 91 | maxSearches = sudoku.Stats.NumSearches 92 | } 93 | sudoku.Stats.Reset() 94 | } 95 | } 96 | 97 | fmt.Printf("Solved %v/%v boards\n", numSolved, numBoards) 98 | fmt.Printf("Average difficulty: %.2v\n", totalDifficulty/float64(numBoards)) 99 | fmt.Printf("Duration average=%-15v max=%v\n", totalDuration/time.Duration(numBoards), maxDuration) 100 | if *statsFlag { 101 | fmt.Printf("Searches average=%-15.2f max=%v\n", float64(totalSearches)/float64(numBoards), maxSearches) 102 | } 103 | } 104 | 105 | func countHints() { 106 | boards := getInputBoards() 107 | for _, board := range boards { 108 | fmt.Println("board:", board) 109 | v, err := sudoku.ParseBoard(board, false) 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | fmt.Println("|") 114 | 115 | initialNumHints := sudoku.CountHints(v) 116 | fmt.Printf("\\ initial num hints: %v\n", initialNumHints) 117 | 118 | sudoku.EliminateAll(v) 119 | afterElimNumHints := sudoku.CountHints(v) 120 | fmt.Printf(" num hints after elimination: %v\n", afterElimNumHints) 121 | 122 | sudoku.ApplyTwinsStrategy(v) 123 | afterTwinsNumHints := sudoku.CountHints(v) 124 | fmt.Printf(" num hints after twins: %v\n", afterTwinsNumHints) 125 | fmt.Println("") 126 | } 127 | } 128 | 129 | // getInputBoards reads input boards from stdin, ignores comments and empty 130 | // lines and returns them. 131 | func getInputBoards() []string { 132 | var boards []string 133 | 134 | scanner := bufio.NewScanner(os.Stdin) 135 | // Expect one board per line, ignoring whitespace and lines starting with '#'. 136 | for scanner.Scan() { 137 | board := strings.TrimSpace(scanner.Text()) 138 | if len(board) == 0 || strings.HasPrefix(board, "#") { 139 | continue 140 | } 141 | boards = append(boards, board) 142 | } 143 | 144 | if err := scanner.Err(); err != nil { 145 | log.Fatal(err) 146 | } 147 | return boards 148 | } 149 | -------------------------------------------------------------------------------- /cmd/wasm/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build serve 2 | 3 | build: 4 | GOOS=js GOARCH=wasm go build -o assets/gosudoku.wasm main.go 5 | 6 | serve: 7 | go run github.com/eliben/static-server@latest -port 8899 assets/ 8 | -------------------------------------------------------------------------------- /cmd/wasm/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
Hint count:
22 |
23 |
24 | 25 | 62 | 63 | -------------------------------------------------------------------------------- /cmd/wasm/assets/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const enosys = () => { 9 | const err = new Error("not implemented"); 10 | err.code = "ENOSYS"; 11 | return err; 12 | }; 13 | 14 | if (!globalThis.fs) { 15 | let outputBuf = ""; 16 | globalThis.fs = { 17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 18 | writeSync(fd, buf) { 19 | outputBuf += decoder.decode(buf); 20 | const nl = outputBuf.lastIndexOf("\n"); 21 | if (nl != -1) { 22 | console.log(outputBuf.substring(0, nl)); 23 | outputBuf = outputBuf.substring(nl + 1); 24 | } 25 | return buf.length; 26 | }, 27 | write(fd, buf, offset, length, position, callback) { 28 | if (offset !== 0 || length !== buf.length || position !== null) { 29 | callback(enosys()); 30 | return; 31 | } 32 | const n = this.writeSync(fd, buf); 33 | callback(null, n); 34 | }, 35 | chmod(path, mode, callback) { callback(enosys()); }, 36 | chown(path, uid, gid, callback) { callback(enosys()); }, 37 | close(fd, callback) { callback(enosys()); }, 38 | fchmod(fd, mode, callback) { callback(enosys()); }, 39 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 40 | fstat(fd, callback) { callback(enosys()); }, 41 | fsync(fd, callback) { callback(null); }, 42 | ftruncate(fd, length, callback) { callback(enosys()); }, 43 | lchown(path, uid, gid, callback) { callback(enosys()); }, 44 | link(path, link, callback) { callback(enosys()); }, 45 | lstat(path, callback) { callback(enosys()); }, 46 | mkdir(path, perm, callback) { callback(enosys()); }, 47 | open(path, flags, mode, callback) { callback(enosys()); }, 48 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 49 | readdir(path, callback) { callback(enosys()); }, 50 | readlink(path, callback) { callback(enosys()); }, 51 | rename(from, to, callback) { callback(enosys()); }, 52 | rmdir(path, callback) { callback(enosys()); }, 53 | stat(path, callback) { callback(enosys()); }, 54 | symlink(path, link, callback) { callback(enosys()); }, 55 | truncate(path, length, callback) { callback(enosys()); }, 56 | unlink(path, callback) { callback(enosys()); }, 57 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 58 | }; 59 | } 60 | 61 | if (!globalThis.process) { 62 | globalThis.process = { 63 | getuid() { return -1; }, 64 | getgid() { return -1; }, 65 | geteuid() { return -1; }, 66 | getegid() { return -1; }, 67 | getgroups() { throw enosys(); }, 68 | pid: -1, 69 | ppid: -1, 70 | umask() { throw enosys(); }, 71 | cwd() { throw enosys(); }, 72 | chdir() { throw enosys(); }, 73 | } 74 | } 75 | 76 | if (!globalThis.crypto) { 77 | throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); 78 | } 79 | 80 | if (!globalThis.performance) { 81 | throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); 82 | } 83 | 84 | if (!globalThis.TextEncoder) { 85 | throw new Error("globalThis.TextEncoder is not available, polyfill required"); 86 | } 87 | 88 | if (!globalThis.TextDecoder) { 89 | throw new Error("globalThis.TextDecoder is not available, polyfill required"); 90 | } 91 | 92 | const encoder = new TextEncoder("utf-8"); 93 | const decoder = new TextDecoder("utf-8"); 94 | 95 | globalThis.Go = class { 96 | constructor() { 97 | this.argv = ["js"]; 98 | this.env = {}; 99 | this.exit = (code) => { 100 | if (code !== 0) { 101 | console.warn("exit code:", code); 102 | } 103 | }; 104 | this._exitPromise = new Promise((resolve) => { 105 | this._resolveExitPromise = resolve; 106 | }); 107 | this._pendingEvent = null; 108 | this._scheduledTimeouts = new Map(); 109 | this._nextCallbackTimeoutID = 1; 110 | 111 | const setInt64 = (addr, v) => { 112 | this.mem.setUint32(addr + 0, v, true); 113 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 114 | } 115 | 116 | const setInt32 = (addr, v) => { 117 | this.mem.setUint32(addr + 0, v, true); 118 | } 119 | 120 | const getInt64 = (addr) => { 121 | const low = this.mem.getUint32(addr + 0, true); 122 | const high = this.mem.getInt32(addr + 4, true); 123 | return low + high * 4294967296; 124 | } 125 | 126 | const loadValue = (addr) => { 127 | const f = this.mem.getFloat64(addr, true); 128 | if (f === 0) { 129 | return undefined; 130 | } 131 | if (!isNaN(f)) { 132 | return f; 133 | } 134 | 135 | const id = this.mem.getUint32(addr, true); 136 | return this._values[id]; 137 | } 138 | 139 | const storeValue = (addr, v) => { 140 | const nanHead = 0x7FF80000; 141 | 142 | if (typeof v === "number" && v !== 0) { 143 | if (isNaN(v)) { 144 | this.mem.setUint32(addr + 4, nanHead, true); 145 | this.mem.setUint32(addr, 0, true); 146 | return; 147 | } 148 | this.mem.setFloat64(addr, v, true); 149 | return; 150 | } 151 | 152 | if (v === undefined) { 153 | this.mem.setFloat64(addr, 0, true); 154 | return; 155 | } 156 | 157 | let id = this._ids.get(v); 158 | if (id === undefined) { 159 | id = this._idPool.pop(); 160 | if (id === undefined) { 161 | id = this._values.length; 162 | } 163 | this._values[id] = v; 164 | this._goRefCounts[id] = 0; 165 | this._ids.set(v, id); 166 | } 167 | this._goRefCounts[id]++; 168 | let typeFlag = 0; 169 | switch (typeof v) { 170 | case "object": 171 | if (v !== null) { 172 | typeFlag = 1; 173 | } 174 | break; 175 | case "string": 176 | typeFlag = 2; 177 | break; 178 | case "symbol": 179 | typeFlag = 3; 180 | break; 181 | case "function": 182 | typeFlag = 4; 183 | break; 184 | } 185 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 186 | this.mem.setUint32(addr, id, true); 187 | } 188 | 189 | const loadSlice = (addr) => { 190 | const array = getInt64(addr + 0); 191 | const len = getInt64(addr + 8); 192 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 193 | } 194 | 195 | const loadSliceOfValues = (addr) => { 196 | const array = getInt64(addr + 0); 197 | const len = getInt64(addr + 8); 198 | const a = new Array(len); 199 | for (let i = 0; i < len; i++) { 200 | a[i] = loadValue(array + i * 8); 201 | } 202 | return a; 203 | } 204 | 205 | const loadString = (addr) => { 206 | const saddr = getInt64(addr + 0); 207 | const len = getInt64(addr + 8); 208 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 209 | } 210 | 211 | const timeOrigin = Date.now() - performance.now(); 212 | this.importObject = { 213 | _gotest: { 214 | add: (a, b) => a + b, 215 | }, 216 | gojs: { 217 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 218 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 219 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 220 | // This changes the SP, thus we have to update the SP used by the imported function. 221 | 222 | // func wasmExit(code int32) 223 | "runtime.wasmExit": (sp) => { 224 | sp >>>= 0; 225 | const code = this.mem.getInt32(sp + 8, true); 226 | this.exited = true; 227 | delete this._inst; 228 | delete this._values; 229 | delete this._goRefCounts; 230 | delete this._ids; 231 | delete this._idPool; 232 | this.exit(code); 233 | }, 234 | 235 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 236 | "runtime.wasmWrite": (sp) => { 237 | sp >>>= 0; 238 | const fd = getInt64(sp + 8); 239 | const p = getInt64(sp + 16); 240 | const n = this.mem.getInt32(sp + 24, true); 241 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 242 | }, 243 | 244 | // func resetMemoryDataView() 245 | "runtime.resetMemoryDataView": (sp) => { 246 | sp >>>= 0; 247 | this.mem = new DataView(this._inst.exports.mem.buffer); 248 | }, 249 | 250 | // func nanotime1() int64 251 | "runtime.nanotime1": (sp) => { 252 | sp >>>= 0; 253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 254 | }, 255 | 256 | // func walltime() (sec int64, nsec int32) 257 | "runtime.walltime": (sp) => { 258 | sp >>>= 0; 259 | const msec = (new Date).getTime(); 260 | setInt64(sp + 8, msec / 1000); 261 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 262 | }, 263 | 264 | // func scheduleTimeoutEvent(delay int64) int32 265 | "runtime.scheduleTimeoutEvent": (sp) => { 266 | sp >>>= 0; 267 | const id = this._nextCallbackTimeoutID; 268 | this._nextCallbackTimeoutID++; 269 | this._scheduledTimeouts.set(id, setTimeout( 270 | () => { 271 | this._resume(); 272 | while (this._scheduledTimeouts.has(id)) { 273 | // for some reason Go failed to register the timeout event, log and try again 274 | // (temporary workaround for https://github.com/golang/go/issues/28975) 275 | console.warn("scheduleTimeoutEvent: missed timeout event"); 276 | this._resume(); 277 | } 278 | }, 279 | getInt64(sp + 8), 280 | )); 281 | this.mem.setInt32(sp + 16, id, true); 282 | }, 283 | 284 | // func clearTimeoutEvent(id int32) 285 | "runtime.clearTimeoutEvent": (sp) => { 286 | sp >>>= 0; 287 | const id = this.mem.getInt32(sp + 8, true); 288 | clearTimeout(this._scheduledTimeouts.get(id)); 289 | this._scheduledTimeouts.delete(id); 290 | }, 291 | 292 | // func getRandomData(r []byte) 293 | "runtime.getRandomData": (sp) => { 294 | sp >>>= 0; 295 | crypto.getRandomValues(loadSlice(sp + 8)); 296 | }, 297 | 298 | // func finalizeRef(v ref) 299 | "syscall/js.finalizeRef": (sp) => { 300 | sp >>>= 0; 301 | const id = this.mem.getUint32(sp + 8, true); 302 | this._goRefCounts[id]--; 303 | if (this._goRefCounts[id] === 0) { 304 | const v = this._values[id]; 305 | this._values[id] = null; 306 | this._ids.delete(v); 307 | this._idPool.push(id); 308 | } 309 | }, 310 | 311 | // func stringVal(value string) ref 312 | "syscall/js.stringVal": (sp) => { 313 | sp >>>= 0; 314 | storeValue(sp + 24, loadString(sp + 8)); 315 | }, 316 | 317 | // func valueGet(v ref, p string) ref 318 | "syscall/js.valueGet": (sp) => { 319 | sp >>>= 0; 320 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 321 | sp = this._inst.exports.getsp() >>> 0; // see comment above 322 | storeValue(sp + 32, result); 323 | }, 324 | 325 | // func valueSet(v ref, p string, x ref) 326 | "syscall/js.valueSet": (sp) => { 327 | sp >>>= 0; 328 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 329 | }, 330 | 331 | // func valueDelete(v ref, p string) 332 | "syscall/js.valueDelete": (sp) => { 333 | sp >>>= 0; 334 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 335 | }, 336 | 337 | // func valueIndex(v ref, i int) ref 338 | "syscall/js.valueIndex": (sp) => { 339 | sp >>>= 0; 340 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 341 | }, 342 | 343 | // valueSetIndex(v ref, i int, x ref) 344 | "syscall/js.valueSetIndex": (sp) => { 345 | sp >>>= 0; 346 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 347 | }, 348 | 349 | // func valueCall(v ref, m string, args []ref) (ref, bool) 350 | "syscall/js.valueCall": (sp) => { 351 | sp >>>= 0; 352 | try { 353 | const v = loadValue(sp + 8); 354 | const m = Reflect.get(v, loadString(sp + 16)); 355 | const args = loadSliceOfValues(sp + 32); 356 | const result = Reflect.apply(m, v, args); 357 | sp = this._inst.exports.getsp() >>> 0; // see comment above 358 | storeValue(sp + 56, result); 359 | this.mem.setUint8(sp + 64, 1); 360 | } catch (err) { 361 | sp = this._inst.exports.getsp() >>> 0; // see comment above 362 | storeValue(sp + 56, err); 363 | this.mem.setUint8(sp + 64, 0); 364 | } 365 | }, 366 | 367 | // func valueInvoke(v ref, args []ref) (ref, bool) 368 | "syscall/js.valueInvoke": (sp) => { 369 | sp >>>= 0; 370 | try { 371 | const v = loadValue(sp + 8); 372 | const args = loadSliceOfValues(sp + 16); 373 | const result = Reflect.apply(v, undefined, args); 374 | sp = this._inst.exports.getsp() >>> 0; // see comment above 375 | storeValue(sp + 40, result); 376 | this.mem.setUint8(sp + 48, 1); 377 | } catch (err) { 378 | sp = this._inst.exports.getsp() >>> 0; // see comment above 379 | storeValue(sp + 40, err); 380 | this.mem.setUint8(sp + 48, 0); 381 | } 382 | }, 383 | 384 | // func valueNew(v ref, args []ref) (ref, bool) 385 | "syscall/js.valueNew": (sp) => { 386 | sp >>>= 0; 387 | try { 388 | const v = loadValue(sp + 8); 389 | const args = loadSliceOfValues(sp + 16); 390 | const result = Reflect.construct(v, args); 391 | sp = this._inst.exports.getsp() >>> 0; // see comment above 392 | storeValue(sp + 40, result); 393 | this.mem.setUint8(sp + 48, 1); 394 | } catch (err) { 395 | sp = this._inst.exports.getsp() >>> 0; // see comment above 396 | storeValue(sp + 40, err); 397 | this.mem.setUint8(sp + 48, 0); 398 | } 399 | }, 400 | 401 | // func valueLength(v ref) int 402 | "syscall/js.valueLength": (sp) => { 403 | sp >>>= 0; 404 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 405 | }, 406 | 407 | // valuePrepareString(v ref) (ref, int) 408 | "syscall/js.valuePrepareString": (sp) => { 409 | sp >>>= 0; 410 | const str = encoder.encode(String(loadValue(sp + 8))); 411 | storeValue(sp + 16, str); 412 | setInt64(sp + 24, str.length); 413 | }, 414 | 415 | // valueLoadString(v ref, b []byte) 416 | "syscall/js.valueLoadString": (sp) => { 417 | sp >>>= 0; 418 | const str = loadValue(sp + 8); 419 | loadSlice(sp + 16).set(str); 420 | }, 421 | 422 | // func valueInstanceOf(v ref, t ref) bool 423 | "syscall/js.valueInstanceOf": (sp) => { 424 | sp >>>= 0; 425 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 426 | }, 427 | 428 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 429 | "syscall/js.copyBytesToGo": (sp) => { 430 | sp >>>= 0; 431 | const dst = loadSlice(sp + 8); 432 | const src = loadValue(sp + 32); 433 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 434 | this.mem.setUint8(sp + 48, 0); 435 | return; 436 | } 437 | const toCopy = src.subarray(0, dst.length); 438 | dst.set(toCopy); 439 | setInt64(sp + 40, toCopy.length); 440 | this.mem.setUint8(sp + 48, 1); 441 | }, 442 | 443 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 444 | "syscall/js.copyBytesToJS": (sp) => { 445 | sp >>>= 0; 446 | const dst = loadValue(sp + 8); 447 | const src = loadSlice(sp + 16); 448 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 449 | this.mem.setUint8(sp + 48, 0); 450 | return; 451 | } 452 | const toCopy = src.subarray(0, dst.length); 453 | dst.set(toCopy); 454 | setInt64(sp + 40, toCopy.length); 455 | this.mem.setUint8(sp + 48, 1); 456 | }, 457 | 458 | "debug": (value) => { 459 | console.log(value); 460 | }, 461 | } 462 | }; 463 | } 464 | 465 | async run(instance) { 466 | if (!(instance instanceof WebAssembly.Instance)) { 467 | throw new Error("Go.run: WebAssembly.Instance expected"); 468 | } 469 | this._inst = instance; 470 | this.mem = new DataView(this._inst.exports.mem.buffer); 471 | this._values = [ // JS values that Go currently has references to, indexed by reference id 472 | NaN, 473 | 0, 474 | null, 475 | true, 476 | false, 477 | globalThis, 478 | this, 479 | ]; 480 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 481 | this._ids = new Map([ // mapping from JS values to reference ids 482 | [0, 1], 483 | [null, 2], 484 | [true, 3], 485 | [false, 4], 486 | [globalThis, 5], 487 | [this, 6], 488 | ]); 489 | this._idPool = []; // unused ids that have been garbage collected 490 | this.exited = false; // whether the Go program has exited 491 | 492 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 493 | let offset = 4096; 494 | 495 | const strPtr = (str) => { 496 | const ptr = offset; 497 | const bytes = encoder.encode(str + "\0"); 498 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 499 | offset += bytes.length; 500 | if (offset % 8 !== 0) { 501 | offset += 8 - (offset % 8); 502 | } 503 | return ptr; 504 | }; 505 | 506 | const argc = this.argv.length; 507 | 508 | const argvPtrs = []; 509 | this.argv.forEach((arg) => { 510 | argvPtrs.push(strPtr(arg)); 511 | }); 512 | argvPtrs.push(0); 513 | 514 | const keys = Object.keys(this.env).sort(); 515 | keys.forEach((key) => { 516 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 517 | }); 518 | argvPtrs.push(0); 519 | 520 | const argv = offset; 521 | argvPtrs.forEach((ptr) => { 522 | this.mem.setUint32(offset, ptr, true); 523 | this.mem.setUint32(offset + 4, 0, true); 524 | offset += 8; 525 | }); 526 | 527 | // The linker guarantees global data starts from at least wasmMinDataAddr. 528 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 529 | const wasmMinDataAddr = 4096 + 8192; 530 | if (offset >= wasmMinDataAddr) { 531 | throw new Error("total length of command line and environment variables exceeds limit"); 532 | } 533 | 534 | this._inst.exports.run(argc, argv); 535 | if (this.exited) { 536 | this._resolveExitPromise(); 537 | } 538 | await this._exitPromise; 539 | } 540 | 541 | _resume() { 542 | if (this.exited) { 543 | throw new Error("Go program has already exited"); 544 | } 545 | this._inst.exports.resume(); 546 | if (this.exited) { 547 | this._resolveExitPromise(); 548 | } 549 | } 550 | 551 | _makeFuncWrapper(id) { 552 | const go = this; 553 | return function () { 554 | const event = { id: id, this: this, args: arguments }; 555 | go._pendingEvent = event; 556 | go._resume(); 557 | return event.result; 558 | }; 559 | } 560 | } 561 | })(); 562 | -------------------------------------------------------------------------------- /cmd/wasm/assets/worker.js: -------------------------------------------------------------------------------- 1 | // Web worker for invoking generateBoard from Go in a separate thread. 2 | importScripts("wasm_exec.js"); 3 | console.log("Worker is running"); 4 | 5 | // Load the WASM module with Go code. 6 | const go = new Go(); 7 | WebAssembly.instantiateStreaming(fetch("gosudoku.wasm"), go.importObject).then( 8 | (result) => { 9 | go.run(result.instance); 10 | console.log("Worker loaded WASM module"); 11 | }).catch((err) => { 12 | console.error("Worker failed to load WASM module: ", err) 13 | }); 14 | 15 | // The worker's logic is very simple: it waits for a "generate" message with 16 | // paramterers, runs the Go code to generate the board and sends a "ready" 17 | // message with the SVG text back to the main thread. 18 | onmessage = ({ data }) => { 19 | let { action, payload } = data; 20 | console.log("Worker received message: ", action, payload); 21 | switch (action) { 22 | case "generate": 23 | let svgText = generateBoard(payload.hint, payload.symmetrical); 24 | postMessage({ action: "boardReady", payload: svgText }); 25 | break; 26 | default: 27 | throw (`unknown action '${action}'`); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /cmd/wasm/main.go: -------------------------------------------------------------------------------- 1 | //go:build js && wasm 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "log" 9 | "syscall/js" 10 | 11 | "github.com/eliben/go-sudoku" 12 | ) 13 | 14 | func main() { 15 | fmt.Println("go-sudoku wasm") 16 | 17 | // Export the jsGenerateBoard function to JS. 18 | js.Global().Set("generateBoard", jsGenerateBoard) 19 | 20 | // For the Go code to be usable from JS, the main function has to run forever. 21 | <-make(chan bool) 22 | } 23 | 24 | // jsGenerateBoard wraps the functionality we need from this package, for use 25 | // in the web interface. It creates a function that takes two parameters: 26 | // an integer hint count, and a boolean "is symmetrical" flag. It returns 27 | // the SVG generated for the board as a string. 28 | var jsGenerateBoard = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 29 | if len(args) != 2 { 30 | return fmt.Sprintf("got %v args, want 2", len(args)) 31 | } 32 | hintCount := args[0].Int() 33 | symmetrical := args[1].Bool() 34 | 35 | var board sudoku.Values 36 | if symmetrical { 37 | board = sudoku.GenerateSymmetrical(hintCount) 38 | } else { 39 | board = sudoku.Generate(hintCount) 40 | } 41 | 42 | d, err := sudoku.EvaluateDifficulty(board) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | var buf bytes.Buffer 48 | sudoku.DisplayAsSVG(&buf, board, d) 49 | return buf.String() 50 | }) 51 | -------------------------------------------------------------------------------- /difficulty.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "fmt" 5 | 6 | "slices" 7 | ) 8 | 9 | // EvaluateDifficulty evaluates the difficulty of a Sudoku puzzle heuristically 10 | // and returns the score on a scale from 1.0 (easiest) to 5.0 hardest. It can 11 | // also return an error if the given board has contradictions, is unsolvable, 12 | // etc. It should be passed a board that didn't have elimination applied to it. 13 | // 14 | // The heuristics are based on 4 factors: 15 | // 16 | // 1. How many hints (filled-in squares) the board has. 17 | // 2. How many hints remain after running a round of elimination (first-order 18 | // Sudoku solving value deduction). 19 | // 3. How many hints does a row or column with the minimal number of hints have 20 | // 4. How many guesses a backtracking search requires to solve the board 21 | // (averaged over multiple runs). 22 | // 23 | // This approach was partially inspired by the paper "Sudoku Puzzles Generating: 24 | // from Easy to Evil" by Xiang-Sun ZHANG's research group. 25 | func EvaluateDifficulty(values Values) (float64, error) { 26 | hintsBeforeElimination := CountHints(values) 27 | 28 | // Count the lower bound (minimal number) of hints in individual rows and 29 | // cols, pre elimination. 30 | minHints := 9 31 | 32 | index := func(row, col int) Index { 33 | return row*9 + col 34 | } 35 | 36 | // ... first the rows. 37 | for row := 0; row < 9; row++ { 38 | rowCount := 0 39 | for col := 0; col < 9; col++ { 40 | if values[index(row, col)].Size() == 1 { 41 | rowCount++ 42 | } 43 | } 44 | if rowCount < minHints { 45 | minHints = rowCount 46 | } 47 | } 48 | 49 | // ... then the columns. 50 | for col := 0; col < 9; col++ { 51 | colCount := 0 52 | for row := 0; row < 9; row++ { 53 | if values[index(row, col)].Size() == 1 { 54 | colCount++ 55 | } 56 | } 57 | if colCount < minHints { 58 | minHints = colCount 59 | } 60 | } 61 | 62 | // Run elimination and count how many hints are on the board after it. 63 | vcopy := slices.Clone(values) 64 | if !EliminateAll(vcopy) { 65 | return 0, fmt.Errorf("contradiction in board") 66 | } 67 | hintsAfterElimination := CountHints(vcopy) 68 | 69 | // Run a number of randomized searches and count the average search count. 70 | EnableStats = true 71 | var totalSearches uint64 = 0 72 | iterations := 10 73 | for i := 0; i < iterations; i++ { 74 | Stats.Reset() 75 | _, solved := Solve(vcopy, SolveOptions{Randomize: true}) 76 | if !solved { 77 | return 0, fmt.Errorf("cannot solve") 78 | } 79 | totalSearches += Stats.NumSearches 80 | } 81 | EnableStats = false 82 | averageSearches := float64(totalSearches) / float64(iterations) 83 | 84 | // Assign difficulty scores based on ranges in each category. 85 | var hintsBeforeDifficulty float64 86 | if hintsBeforeElimination > 50 { 87 | hintsBeforeDifficulty = 1.0 88 | } else if hintsBeforeElimination > 35 { 89 | hintsBeforeDifficulty = 2.0 90 | } else if hintsBeforeElimination > 31 { 91 | hintsBeforeDifficulty = 3.0 92 | } else if hintsBeforeElimination > 27 { 93 | hintsBeforeDifficulty = 4.0 94 | } else { 95 | hintsBeforeDifficulty = 5.0 96 | } 97 | 98 | var hintsAfterDifficulty float64 99 | if hintsAfterElimination > 55 { 100 | hintsAfterDifficulty = 1.0 101 | } else if hintsAfterElimination > 42 { 102 | hintsAfterDifficulty = 2.0 103 | } else if hintsAfterElimination > 37 { 104 | hintsAfterDifficulty = 3.0 105 | } else if hintsAfterElimination > 33 { 106 | hintsAfterDifficulty = 4.0 107 | } else { 108 | hintsAfterDifficulty = 5.0 109 | } 110 | 111 | var minHintsDifficulty float64 112 | if minHints >= 5 { 113 | minHintsDifficulty = 1.0 114 | } else if minHints == 4 { 115 | minHintsDifficulty = 2.0 116 | } else if minHints == 3 { 117 | minHintsDifficulty = 3.0 118 | } else if minHints >= 1 { 119 | minHintsDifficulty = 4.0 120 | } else { 121 | minHintsDifficulty = 5.0 122 | } 123 | 124 | var searchDifficulty float64 125 | if averageSearches <= 1.0 { 126 | searchDifficulty = 1.0 127 | } else if averageSearches < 3.0 { 128 | searchDifficulty = 2.0 129 | } else if averageSearches < 10.0 { 130 | searchDifficulty = 3.0 131 | } else if averageSearches < 40.0 { 132 | searchDifficulty = 4.0 133 | } else { 134 | searchDifficulty = 5.0 135 | } 136 | 137 | // Assign final difficulty with weights 138 | difficulty := 0.5*hintsAfterDifficulty + 139 | 0.3*hintsBeforeDifficulty + 140 | 0.05*minHintsDifficulty + 141 | 0.15*searchDifficulty 142 | 143 | return difficulty, nil 144 | } 145 | -------------------------------------------------------------------------------- /difficulty_test.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "slices" 8 | ) 9 | 10 | func TestEvaluateDifficulty(t *testing.T) { 11 | getDifficulty := func(board string) float64 { 12 | v, err := ParseBoard(board, false) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | vcopy := slices.Clone(v) 17 | 18 | d, err := EvaluateDifficulty(v) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if !slices.Equal(v, vcopy) { 24 | t.Errorf("EvaluateDifficulty modified values") 25 | } 26 | 27 | return d 28 | } 29 | 30 | easyD := getDifficulty(easyboard1) 31 | hardD := getDifficulty(hardboard2) 32 | hardlongD := getDifficulty(hardlong) 33 | 34 | if easyD > hardD || hardD > hardlongD { 35 | t.Errorf("got easyD: %v, hardD: %v, hardlongD: %v", easyD, hardD, hardlongD) 36 | } 37 | } 38 | 39 | var filled string = ` 40 | 3 4 5 |7 9 2 |6 1 8 41 | 8 9 6 |3 5 1 |2 7 4 42 | 1 7 2 |8 6 4 |9 5 3 43 | ------+------+------ 44 | 2 3 8 |6 4 5 |1 9 7 45 | 7 5 4 |9 1 8 |3 2 6 46 | 6 1 9 |2 3 7 |4 8 5 47 | ------+------+------ 48 | 5 6 7 |4 2 9 |8 3 1 49 | 4 2 1 |5 8 3 |7 6 9 50 | 9 8 3 |1 7 6 |5 4 2 51 | ` 52 | 53 | func TestFilled(t *testing.T) { 54 | v, err := ParseBoard(filled, false) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | d, err := EvaluateDifficulty(v) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | if d != 1.0 { 65 | t.Errorf("got d=%v; expect difficulty of filled board to be 1.0", d) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /digits.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "fmt" 5 | "math/bits" 6 | "strings" 7 | ) 8 | 9 | // Digits represents a set of possible digits for a Sudoku square. The functions 10 | // in this file perform set operations on Digits, as needed for Sudoku. 11 | type Digits uint16 12 | 13 | // Internal representation: digit N is represented by the Nth lowest bit in 14 | // the Digits value, e.g.: 15 | // 16 | // Digits = 0b0000_0000_0110_0010 17 | // 18 | // has the bits N=1,5,6 are set, so it represents the set of digits {1, 5, 6} 19 | 20 | // FullDigitsSet returns a Digits with all possible digits set. 21 | func FullDigitsSet() Digits { 22 | return 0b0000001111111110 23 | } 24 | 25 | // SingleDigitSet returns a Digits with a single digit 'n' set. 26 | func SingleDigitSet(n uint16) Digits { 27 | return 1 << n 28 | } 29 | 30 | // IsMember checks whether digit n is a member of the digit set d. 31 | func (d Digits) IsMember(n uint16) bool { 32 | return (d & (1 << n)) != 0 33 | } 34 | 35 | // Add adds digit n to set d and returns the new set. 36 | func (d Digits) Add(n uint16) Digits { 37 | return d | (1 << n) 38 | } 39 | 40 | // Remove removes digit n from set d and returns the new set. 41 | func (d Digits) Remove(n uint16) Digits { 42 | return d &^ (1 << n) 43 | } 44 | 45 | // RemoveAll removes all digits represented by dn from d and returns 46 | // the new set. 47 | func (d Digits) RemoveAll(dn Digits) Digits { 48 | return d &^ dn 49 | } 50 | 51 | // Size returns the size of the set - the number of digits in it. 52 | func (d Digits) Size() int { 53 | return bits.OnesCount16(uint16(d)) 54 | } 55 | 56 | // SingleMemberDigit returns the digit that's a member of a 1-element set; this 57 | // assumes that the set indeed has a single element. 58 | func (d Digits) SingleMemberDigit() uint16 { 59 | return uint16(bits.TrailingZeros16(uint16(d))) 60 | } 61 | 62 | // twoMemberDigits returns the only two digits that are member of a 2-element 63 | // set; this assumes that the set indeed has two elements. 64 | func (d Digits) twoMemberDigits() (uint16, uint16) { 65 | d1 := uint16(bits.TrailingZeros16(uint16(d))) 66 | d2 := 16 - uint16(bits.LeadingZeros16(uint16(d))) - 1 67 | return d1, d2 68 | } 69 | 70 | // String implements the fmt.Stringer interface for Digits. 71 | func (d Digits) String() string { 72 | var parts []string 73 | for i := uint16(1); i <= uint16(9); i++ { 74 | if d.IsMember(i) { 75 | parts = append(parts, fmt.Sprintf("%v", i)) 76 | } 77 | } 78 | return strings.Join(parts, "") 79 | } 80 | -------------------------------------------------------------------------------- /digits_test.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestIsMember(t *testing.T) { 9 | d := Digits(0b0000000001100010) 10 | 11 | var i uint16 12 | for i = 1; i <= 9; i++ { 13 | if i == 1 || i == 5 || i == 6 { 14 | if !d.IsMember(i) { 15 | t.Errorf("got isMember(%016b, %d)=false, want true", d, i) 16 | } 17 | } else { 18 | if d.IsMember(i) { 19 | t.Errorf("got isMember(%016b, %d)=true, want false", d, i) 20 | } 21 | } 22 | } 23 | } 24 | 25 | func TestAdd(t *testing.T) { 26 | d := Digits(0b0000000001100010) 27 | 28 | if d.String() != "156" { 29 | t.Errorf("got %v, want 156", d.String()) 30 | } 31 | 32 | wantd2 := Digits(0b0000000001100110) 33 | if d.Add(2) != wantd2 { 34 | t.Errorf("got wantd2=%v, want=%v", d.Add(2), wantd2) 35 | } 36 | 37 | wantd5 := Digits(0b0000000001100010) 38 | if d.Add(5) != wantd5 { 39 | t.Errorf("got wantd5=%v, want=%v", d.Add(5), wantd5) 40 | } 41 | } 42 | 43 | func TestRemove(t *testing.T) { 44 | d := Digits(0b0000000001100010) 45 | 46 | wantd5 := Digits(0b0000000001000010) 47 | if d.Remove(5) != wantd5 { 48 | t.Errorf("got wantd5=%v, want=%v", d.Remove(5), wantd5) 49 | } 50 | 51 | wantd8 := Digits(0b0000000001100010) 52 | if d.Remove(8) != wantd8 { 53 | t.Errorf("got wantd8=%v, want=%v", d.Remove(8), wantd8) 54 | } 55 | 56 | wantd6no1 := Digits(0b0000000001000000) 57 | donly6 := d.Remove(1).Remove(5) 58 | if donly6 != wantd6no1 { 59 | t.Errorf("got wantd6no1=%v, want=%v", donly6, wantd6no1) 60 | } 61 | 62 | if donly6.String() != "6" { 63 | t.Errorf("got %v, want 6", donly6.String()) 64 | } 65 | } 66 | 67 | func TestRemoveAll(t *testing.T) { 68 | d := Digits(0b0011100110) 69 | 70 | got1 := d.RemoveAll(0b0011000000) 71 | want1 := Digits(0b0000100110) 72 | if got1 != want1 { 73 | t.Errorf("got %v, want %v", got1, want1) 74 | } 75 | 76 | got2 := d.RemoveAll(0b0001000110) 77 | want2 := Digits(0b0010100000) 78 | if got2 != want2 { 79 | t.Errorf("got %v, want %v", got2, want2) 80 | } 81 | } 82 | 83 | func TestAddRemoveAllSize(t *testing.T) { 84 | // Exhaustive testing that adds/removes every digits and tests that IsMember 85 | // also keeps working. 86 | 87 | // Start with zero. Each iteration adds one digit, tests membership, then 88 | // removes the digit and tests again. 89 | d := Digits(0) 90 | 91 | testNoMembers := func() { 92 | if d.Size() != 0 { 93 | t.Errorf("got size=%v, want 0", d.Size()) 94 | } 95 | 96 | for dig := uint16(1); dig <= 9; dig++ { 97 | if d.IsMember(dig) { 98 | t.Errorf("got IsMember=true for %v, want false", dig) 99 | } 100 | } 101 | } 102 | testNoMembers() 103 | 104 | for dig := uint16(1); dig <= 9; dig++ { 105 | t.Run(fmt.Sprintf("dig=%v", dig), func(t *testing.T) { 106 | // Add 'dig' to set 107 | d = d.Add(dig) 108 | 109 | if d.Size() != 1 { 110 | t.Errorf("got size=%v, want 1", d.Size()) 111 | } 112 | 113 | off := d.SingleMemberDigit() 114 | if off != dig { 115 | t.Errorf("got SingleMemberDigit=%v, want %v", off, dig) 116 | } 117 | 118 | // For each 'dig2', check set membership 119 | for dig2 := uint16(1); dig2 <= 9; dig2++ { 120 | if dig2 == dig { 121 | if !d.IsMember(dig2) { 122 | t.Errorf("got IsMember=false for %v, want true", dig2) 123 | } 124 | } else { 125 | if d.IsMember(dig2) { 126 | t.Errorf("got IsMember=true for %v, want false", dig2) 127 | } 128 | } 129 | } 130 | 131 | d = d.Remove(dig) 132 | testNoMembers() 133 | }) 134 | } 135 | } 136 | 137 | func TestFullDigitsSet(t *testing.T) { 138 | d := FullDigitsSet() 139 | for dig := uint16(1); dig <= 9; dig++ { 140 | if !d.IsMember(dig) { 141 | t.Errorf("got IsMember=false for %v, want true", dig) 142 | } 143 | } 144 | 145 | if d.String() != "123456789" { 146 | t.Errorf("got %v, want all digits", d.String()) 147 | } 148 | } 149 | 150 | func TestSingleDigitSet(t *testing.T) { 151 | d := SingleDigitSet(5) 152 | for dig := uint16(1); dig <= 9; dig++ { 153 | if dig == 5 { 154 | if !d.IsMember(dig) { 155 | t.Errorf("got IsMember=false for 5, want true") 156 | } 157 | } else { 158 | if d.IsMember(dig) { 159 | t.Errorf("got IsMember=true for %v, want false", dig) 160 | } 161 | } 162 | } 163 | } 164 | 165 | func TestTwoMemberDigits(t *testing.T) { 166 | d := Digits(0b0000000000100100) 167 | d1, d2 := d.twoMemberDigits() 168 | if d1 != 2 || d2 != 5 { 169 | t.Errorf("got %v,%v, want 2 and 5", d1, d2) 170 | } 171 | 172 | d = Digits(0b0000001000000010) 173 | d1, d2 = d.twoMemberDigits() 174 | if d1 != 1 || d2 != 9 { 175 | t.Errorf("got %v,%v, want 1 and 9", d1, d2) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /doc/genb.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 7 16 | 17 | 6 18 | 19 | 20 | 8 21 | 22 | 23 | 24 | 25 | 26 | 1 27 | 28 | 29 | 30 | 4 31 | 32 | 7 33 | 34 | 35 | 2 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 7 46 | 47 | 9 48 | 49 | 50 | 51 | 52 | 53 | 2 54 | 55 | 56 | 57 | 58 | 4 59 | 60 | 61 | 62 | 63 | 64 | 3 65 | 66 | 67 | 68 | 5 69 | 70 | 8 71 | 72 | 2 73 | 74 | 75 | 76 | 4 77 | 78 | 1 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 2 88 | 89 | 90 | 6 91 | 92 | 93 | 3 94 | 95 | 96 | 5 97 | 98 | 99 | 100 | 8 101 | 102 | 103 | 5 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Difficulty: 4.70 out of 5 119 | 120 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | ) 7 | 8 | // Generate generates a random Sudoku board that has a single solution, with 9 | // at-most hintCount hints remaining on the board. Note that this cannot be 10 | // always reliably done when the count is low (lower than 23 or so), because 11 | // generating a board with a single solution that has a low number of initial 12 | // hints is very hard. 13 | // There are no guarantees made about the difficulty of the generated board, 14 | // though higher hint counts generally correlate with easier boards. It's 15 | // recommended to generate a large number of boards using this function and 16 | // evaluate their difficulty separately using EvaluateDifficulty. 17 | // Notes: 18 | // * Make sure the default rand source is seeded if you really want to get 19 | // random boards. 20 | // * This function may take a while to run when given a low hintCount. 21 | func Generate(hintCount int) Values { 22 | empty := EmptyBoard() 23 | board, solved := Solve(empty, SolveOptions{Randomize: true}) 24 | if !solved || !IsSolved(board) { 25 | log.Fatal("unable to generate solved board from empty") 26 | } 27 | 28 | removalOrder := rand.Perm(81) 29 | count := 81 30 | 31 | for _, sq := range removalOrder { 32 | savedDigit := board[sq] 33 | // Try to remove the number from square sq. 34 | board[sq] = FullDigitsSet() 35 | 36 | solutions := SolveAll(board, 2) 37 | switch len(solutions) { 38 | case 0: 39 | // Some sort of bug, because removing a square from a solved board should 40 | // never result in an unsolvable board. 41 | log.Fatal("got a board without solutions") 42 | case 1: 43 | count-- 44 | if count <= hintCount { 45 | return board 46 | } 47 | default: 48 | // The board has multiple solutions with this square emptied, so put it 49 | // back and try again with the next square. 50 | board[sq] = savedDigit 51 | } 52 | } 53 | 54 | return board 55 | } 56 | 57 | // GenerateSymmetrical is similar to Generate, but it generates symmetrical 58 | // boards with 180-degree rotational symmetry. 59 | // Because of this additional constraint, it may have more trouble generating 60 | // boards with a small hintCount than Generate, so you'll have to run it more 61 | // times in a loop to find a good low-hint-count board. 62 | func GenerateSymmetrical(hintCount int) Values { 63 | empty := EmptyBoard() 64 | board, solved := Solve(empty, SolveOptions{Randomize: true}) 65 | if !solved || !IsSolved(board) { 66 | log.Fatal("unable to generate solved board from empty") 67 | } 68 | 69 | // This function works just like Generate, but instead of picking a random 70 | // square out of all 81, it picks a random square from the first half of the 71 | // board and then attempts to remove both this square and its reflection. 72 | removalOrder := rand.Perm(41) 73 | count := 81 74 | 75 | for _, sq := range removalOrder { 76 | // Find sq's reflection; note that in the middle row reflectSq could equal 77 | // sq - we take this into account when counting how many hints remain on 78 | // the board. 79 | reflectSq := 80 - sq 80 | 81 | savedDigit := board[sq] 82 | savedReflect := board[reflectSq] 83 | 84 | board[sq] = FullDigitsSet() 85 | board[reflectSq] = FullDigitsSet() 86 | 87 | solutions := SolveAll(board, 2) 88 | switch len(solutions) { 89 | case 0: 90 | log.Fatal("got a board without solutions") 91 | case 1: 92 | // We may have removed just one or two hints. 93 | count-- 94 | if sq != reflectSq { 95 | count-- 96 | } 97 | if count <= hintCount { 98 | return board 99 | } 100 | default: 101 | board[sq] = savedDigit 102 | board[reflectSq] = savedReflect 103 | } 104 | } 105 | 106 | return board 107 | } 108 | -------------------------------------------------------------------------------- /generator_test.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestGenerate(t *testing.T) { 10 | board := Generate(30) 11 | vs := SolveAll(board, -1) 12 | if len(vs) != 1 { 13 | t.Errorf("got %v solutions, want 1", len(vs)) 14 | } 15 | 16 | if !IsSolved(vs[0]) { 17 | t.Errorf("got unsolved board") 18 | } 19 | } 20 | 21 | func TestGenerateSymmetrical(t *testing.T) { 22 | rand.Seed(time.Now().UnixNano()) 23 | //for { 24 | board := GenerateSymmetrical(30) 25 | vs := SolveAll(board, -1) 26 | if len(vs) != 1 { 27 | t.Errorf("got %v solutions, want 1", len(vs)) 28 | } 29 | 30 | if !IsSolved(vs[0]) { 31 | t.Errorf("got unsolved board") 32 | } 33 | 34 | // Check symmetry 35 | for sq := 0; sq < 41; sq++ { 36 | if board[sq].Size() != board[80-sq].Size() { 37 | t.Errorf("squares %v != %v on board, expected symmetry", sq, 80-sq) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eliben/go-sudoku 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eliben/go-sudoku/39a7f6d7e78320c6fcaec00c987d6c853f3b820e/go.sum -------------------------------------------------------------------------------- /inputs/norvig-easy50.txt: -------------------------------------------------------------------------------- 1 | # Norvig's "easy 50" puzzles, translated to this format 2 | 003020600900305001001806400008102900700000008006708200002609500800203009005010300 3 | 200080300060070084030500209000105408000000000402706000301007040720040060004010003 4 | 000000907000420180000705026100904000050000040000507009920108000034059000507000000 5 | 030050040008010500460000012070502080000603000040109030250000098001020600080060020 6 | 020810740700003100090002805009040087400208003160030200302700060005600008076051090 7 | 100920000524010000000000070050008102000000000402700090060000000000030945000071006 8 | 043080250600000000000001094900004070000608000010200003820500000000000005034090710 9 | 480006902002008001900370060840010200003704100001060049020085007700900600609200018 10 | 000900002050123400030000160908000000070000090000000205091000050007439020400007000 11 | 001900003900700160030005007050000009004302600200000070600100030042007006500006800 12 | 000125400008400000420800000030000095060902010510000060000003049000007200001298000 13 | 062340750100005600570000040000094800400000006005830000030000091006400007059083260 14 | 300000000005009000200504000020000700160000058704310600000890100000067080000005437 15 | 630000000000500008005674000000020000003401020000000345000007004080300902947100080 16 | 000020040008035000000070602031046970200000000000501203049000730000000010800004000 17 | 361025900080960010400000057008000471000603000259000800740000005020018060005470329 18 | 050807020600010090702540006070020301504000908103080070900076205060090003080103040 19 | 080005000000003457000070809060400903007010500408007020901020000842300000000100080 20 | 003502900000040000106000305900251008070408030800763001308000104000020000005104800 21 | 000000000009805100051907420290401065000000000140508093026709580005103600000000000 22 | 020030090000907000900208005004806500607000208003102900800605007000309000030020050 23 | 005000006070009020000500107804150000000803000000092805907006000030400010200000600 24 | 040000050001943600009000300600050002103000506800020007005000200002436700030000040 25 | 004000000000030002390700080400009001209801307600200008010008053900040000000000800 26 | 360020089000361000000000000803000602400603007607000108000000000000418000970030014 27 | 500400060009000800640020000000001008208000501700500000000090084003000600060003002 28 | 007256400400000005010030060000508000008060200000107000030070090200000004006312700 29 | 000000000079050180800000007007306800450708096003502700700000005016030420000000000 30 | 030000080009000500007509200700105008020090030900402001004207100002000800070000090 31 | 200170603050000100000006079000040700000801000009050000310400000005000060906037002 32 | 000000080800701040040020030374000900000030000005000321010060050050802006080000000 33 | 000000085000210009960080100500800016000000000890006007009070052300054000480000000 34 | 608070502050608070002000300500090006040302050800050003005000200010704090409060701 35 | 050010040107000602000905000208030501040070020901080406000401000304000709020060010 36 | 053000790009753400100000002090080010000907000080030070500000003007641200061000940 37 | 006080300049070250000405000600317004007000800100826009000702000075040190003090600 38 | 005080700700204005320000084060105040008000500070803010450000091600508007003010600 39 | 000900800128006400070800060800430007500000009600079008090004010003600284001007000 40 | 000080000270000054095000810009806400020403060006905100017000620460000038000090000 41 | 000602000400050001085010620038206710000000000019407350026040530900020007000809000 42 | 000900002050123400030000160908000000070000090000000205091000050007439020400007000 43 | 380000000000400785009020300060090000800302009000040070001070500495006000000000092 44 | 000158000002060800030000040027030510000000000046080790050000080004070100000325000 45 | 010500200900001000002008030500030007008000500600080004040100700000700006003004050 46 | 080000040000469000400000007005904600070608030008502100900000005000781000060000010 47 | 904200007010000000000706500000800090020904060040002000001607000000000030300005702 48 | 000700800006000031040002000024070000010030080000060290000800070860000500002006000 49 | 001007090590080001030000080000005800050060020004100000080000030100020079020700400 50 | 000003017015009008060000000100007000009000200000500004000000020500600340340200000 51 | 300200000000107000706030500070009080900020004010800050009040301000702000000008006 52 | 53 | -------------------------------------------------------------------------------- /inputs/norvig-hard.txt: -------------------------------------------------------------------------------- 1 | # These are taken from Norvig's collection of hard puzzles found online; a 2 | # combination of http://norvig.com/top95.txt and https://norvig.com/hardest.txt 3 | 4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4...... 4 | 52...6.........7.13...........4..8..6......5...........418.........3..2...87..... 5 | 6.....8.3.4.7.................5.4.7.3..2.....1.6.......2.....5.....8.6......1.... 6 | 48.3............71.2.......7.5....6....2..8.............1.76...3.....4......5.... 7 | ....14....3....2...7..........9...3.6.1.............8.2.....1.4....5.6.....7.8... 8 | ......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3. 9 | 6.2.5.........3.4..........43...8....1....2........7..5..27...........81...6..... 10 | .524.........7.1..............8.2...3.....6...9.5.....1.6.3...........897........ 11 | 6.2.5.........4.3..........43...8....1....2........7..5..27...........81...6..... 12 | .923.........8.1...........1.7.4...........658.........6.5.2...4.....7.....9..... 13 | 6..3.2....5.....1..........7.26............543.........8.15........4.2........7.. 14 | .6.5.1.9.1...9..539....7....4.8...7.......5.8.817.5.3.....5.2............76..8... 15 | ..5...987.4..5...1..7......2...48....9.1.....6..2.....3..6..2.......9.7.......5.. 16 | 3.6.7...........518.........1.4.5...7.....6.....2......2.....4.....8.3.....5..... 17 | 1.....3.8.7.4..............2.3.1...........958.........5.6...7.....8.2...4....... 18 | 6..3.2....4.....1..........7.26............543.........8.15........4.2........7.. 19 | ....3..9....2....1.5.9..............1.2.8.4.6.8.5...2..75......4.1..6..3.....4.6. 20 | 45.....3....8.1....9...........5..9.2..7.....8.........1..4..........7.2...6..8.. 21 | .237....68...6.59.9.....7......4.97.3.7.96..2.........5..47.........2....8....... 22 | ..84...3....3.....9....157479...8........7..514.....2...9.6...2.5....4......9..56 23 | .98.1....2......6.............3.2.5..84.........6.........4.8.93..5...........1.. 24 | ..247..58..............1.4.....2...9528.9.4....9...1.........3.3....75..685..2... 25 | 4.....8.5.3..........7......2.....6.....5.4......1.......6.3.7.5..2.....1.9...... 26 | .2.3......63.....58.......15....9.3....7........1....8.879..26......6.7...6..7..4 27 | 1.....7.9.4...72..8.........7..1..6.3.......5.6..4..2.........8..53...7.7.2....46 28 | 4.....3.....8.2......7........1...8734.......6........5...6........1.4...82...... 29 | .......71.2.8........4.3...7...6..5....2..3..9........6...7.....8....4......5.... 30 | 6..3.2....4.....8..........7.26............543.........8.15........8.2........7.. 31 | .47.8...1............6..7..6....357......5....1..6....28..4.....9.1...4.....2.69. 32 | ......8.17..2........5.6......7...5..1....3...8.......5......2..4..8....6...3.... 33 | 38.6.......9.......2..3.51......5....3..1..6....4......17.5..8.......9.......7.32 34 | ...5...........5.697.....2...48.2...25.1...3..8..3.........4.7..13.5..9..2...31.. 35 | .2.......3.5.62..9.68...3...5..........64.8.2..47..9....3.....1.....6...17.43.... 36 | .8..4....3......1........2...5...4.69..1..8..2...........3.9....6....5.....2..... 37 | ..8.9.1...6.5...2......6....3.1.7.5.........9..4...3...5....2...7...3.8.2..7....4 38 | 4.....5.8.3..........7......2.....6.....5.8......1.......6.3.7.5..2.....1.8...... 39 | 1.....3.8.6.4..............2.3.1...........958.........5.6...7.....8.2...4....... 40 | 1....6.8..64..........4...7....9.6...7.4..5..5...7.1...5....32.3....8...4........ 41 | 249.6...3.3....2..8.......5.....6......2......1..4.82..9.5..7....4.....1.7...3... 42 | ...8....9.873...4.6..7.......85..97...........43..75.......3....3...145.4....2..1 43 | ...5.1....9....8...6.......4.1..........7..9........3.8.....1.5...2..4.....36.... 44 | ......8.16..2........7.5......6...2..1....3...8.......2......7..3..8....5...4.... 45 | .476...5.8.3.....2.....9......8.5..6...1.....6.24......78...51...6....4..9...4..7 46 | .....7.95.....1...86..2.....2..73..85......6...3..49..3.5...41724................ 47 | .4.5.....8...9..3..76.2.....146..........9..7.....36....1..4.5..6......3..71..2.. 48 | .834.........7..5...........4.1.8..........27...3.....2.6.5....5.....8........1.. 49 | ..9.....3.....9...7.....5.6..65..4.....3......28......3..75.6..6...........12.3.8 50 | .26.39......6....19.....7.......4..9.5....2....85.....3..2..9..4....762.........4 51 | 2.3.8....8..7...........1...6.5.7...4......3....1............82.5....6...1....... 52 | 6..3.2....1.....5..........7.26............843.........8.15........8.2........7.. 53 | 1.....9...64..1.7..7..4.......3.....3.89..5....7....2.....6.7.9.....4.1....129.3. 54 | .........9......84.623...5....6...453...1...6...9...7....1.....4.5..2....3.8....9 55 | .2....5938..5..46.94..6...8..2.3.....6..8.73.7..2.........4.38..7....6..........5 56 | 9.4..5...25.6..1..31......8.7...9...4..26......147....7.......2...3..8.6.4.....9. 57 | ...52.....9...3..4......7...1.....4..8..453..6...1...87.2........8....32.4..8..1. 58 | 53..2.9...24.3..5...9..........1.827...7.........981.............64....91.2.5.43. 59 | 1....786...7..8.1.8..2....9........24...1......9..5...6.8..........5.9.......93.4 60 | ....5...11......7..6.....8......4.....9.1.3.....596.2..8..62..7..7......3.5.7.2.. 61 | .47.2....8....1....3....9.2.....5...6..81..5.....4.....7....3.4...9...1.4..27.8.. 62 | ......94.....9...53....5.7..8.4..1..463...........7.8.8..7.....7......28.5.26.... 63 | .2......6....41.....78....1......7....37.....6..412....1..74..5..8.5..7......39.. 64 | 1.....3.8.6.4..............2.3.1...........758.........7.5...6.....8.2...4....... 65 | 2....1.9..1..3.7..9..8...2.......85..6.4.........7...3.2.3...6....5.....1.9...2.5 66 | ..7..8.....6.2.3...3......9.1..5..6.....1.....7.9....2........4.83..4...26....51. 67 | ...36....85.......9.4..8........68.........17..9..45...1.5...6.4....9..2.....3... 68 | 34.6.......7.......2..8.57......5....7..1..2....4......36.2..1.......9.......7.82 69 | ......4.18..2........6.7......8...6..4....3...1.......6......2..5..1....7...3.... 70 | .4..5..67...1...4....2.....1..8..3........2...6...........4..5.3.....8..2........ 71 | .......4...2..4..1.7..5..9...3..7....4..6....6..1..8...2....1..85.9...6.....8...3 72 | 8..7....4.5....6............3.97...8....43..5....2.9....6......2...6...7.71..83.2 73 | .8...4.5....7..3............1..85...6.....2......4....3.26............417........ 74 | ....7..8...6...5...2...3.61.1...7..2..8..534.2..9.......2......58...6.3.4...1.... 75 | ......8.16..2........7.5......6...2..1....3...8.......2......7..4..8....5...3.... 76 | .2..........6....3.74.8.........3..2.8..4..1.6..5.........1.78.5....9..........4. 77 | .52..68.......7.2.......6....48..9..2..41......1.....8..61..38.....9...63..6..1.9 78 | ....1.78.5....9..........4..2..........6....3.74.8.........3..2.8..4..1.6..5..... 79 | 1.......3.6.3..7...7...5..121.7...9...7........8.1..2....8.64....9.2..6....4..... 80 | 4...7.1....19.46.5.....1......7....2..2.3....847..6....14...8.6.2....3..6...9.... 81 | ......8.17..2........5.6......7...5..1....3...8.......5......2..3..8....6...4.... 82 | 963......1....8......2.5....4.8......1....7......3..257......3...9.2.4.7......9.. 83 | 15.3......7..4.2....4.72.....8.........9..1.8.1..8.79......38...........6....7423 84 | ..........5724...98....947...9..3...5..9..12...3.1.9...6....25....56.....7......6 85 | ....75....1..2.....4...3...5.....3.2...8...1.......6.....1..48.2........7........ 86 | 6.....7.3.4.8.................5.4.8.7..2.....1.3.......2.....5.....7.9......1.... 87 | ....6...4..6.3....1..4..5.77.....8.5...8.....6.8....9...2.9....4....32....97..1.. 88 | .32.....58..3.....9.428...1...4...39...6...5.....1.....2...67.8.....4....95....6. 89 | ...5.3.......6.7..5.8....1636..2.......4.1.......3...567....2.8..4.7.......2..5.. 90 | .5.3.7.4.1.........3.......5.8.3.61....8..5.9.6..1........4...6...6927....2...9.. 91 | ..5..8..18......9.......78....4.....64....9......53..2.6.........138..5....9.714. 92 | ..........72.6.1....51...82.8...13..4.........37.9..1.....238..5.4..9.........79. 93 | ...658.....4......12............96.7...3..5....2.8...3..19..8..3.6.....4....473.. 94 | .2.3.......6..8.9.83.5........2...8.7.9..5........6..4.......1...1...4.22..7..8.9 95 | .5..9....1.....6.....3.8.....8.4...9514.......3....2..........4.8...6..77..15..6. 96 | .....2.......7...17..3...9.8..7......2.89.6...13..6....9..5.824.....891.......... 97 | 3...8.......7....51..............36...2..4....7...........6.13..452...........8.. 98 | 85...24..72......9..4.........1.7..23.5...9...4...........8..7..17..........36.4. 99 | ..53.....8......2..7..1.5..4....53...1..7...6..32...8..6.5....9..4....3......97.. 100 | 12..4......5.69.1...9...5.........7.7...52.9..3......2.9.6...5.4..9..8.1..3...9.4 101 | ...57..3.1......2.7...234......8...4..7..4...49....6.5.42...3.....7..9....18..... 102 | 7..1523........92....3.....1....47.8.......6............9...5.6.4.9.7...8....6.1. 103 | 1....7.9..3..2...8..96..5....53..9...1..8...26....4...3......1..4......7..7...3.. 104 | 1...34.8....8..5....4.6..21.18......3..1.2..6......81.52..7.9....6..9....9.64...2 105 | ...92......68.3...19..7...623..4.1....1...7....8.3..297...8..91...5.72......64... 106 | .6.5.4.3.1...9...8.........9...5...6.4.6.2.7.7...4...5.........4...8...1.5.2.3.4. 107 | 7.....4...2..7..8...3..8.799..5..3...6..2..9...1.97..6...3..9...3..4..6...9..1.35 108 | ....7..2.8.......6.1.2.5...9.54....8.........3....85.1...3.2.8.4.......9.7..6.... 109 | -------------------------------------------------------------------------------- /inputs/norvig-hardlong.txt: -------------------------------------------------------------------------------- 1 | # The hardest board Norvig generated in a million random puzzles. Note: it has 2 | # more than one solution. 3 | .....6....59.....82....8....45........3........6..3.54...325..6.................. 4 | -------------------------------------------------------------------------------- /sudoku.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math/rand" 7 | "strings" 8 | 9 | "slices" 10 | 11 | "github.com/eliben/go-sudoku/svg" 12 | ) 13 | 14 | // Index represents a square on the Sudoku board; it's a number in the inclusive 15 | // range [0, 80] that stands for row*9+col. 16 | // 17 | // These are the squares designated by an Index: 18 | // 19 | // 0 1 2 | 3 4 5 | 6 7 8 20 | // 9 10 11 | 12 13 14 | 15 16 17 21 | // 22 | // 18 19 20 | 21 22 23 | 24 25 26 23 | // ---------+----------+--------- 24 | // 27 28 29 | 30 31 32 | 33 34 35 25 | // 36 37 38 | 39 40 41 | 42 43 44 26 | // 45 46 47 | 48 49 50 | 51 52 53 27 | // ---------+----------+--------- 28 | // 54 55 56 | 57 58 59 | 60 61 62 29 | // 63 64 65 | 66 67 68 | 69 70 71 30 | // 72 73 74 | 75 76 77 | 78 79 80 31 | type Index = int 32 | 33 | // Unit is a list of square indices that belong to the same Sudoku 34 | // unit - a row, column or 3x3 block which should contain unique digits. 35 | // There are overlaps between many units - e.g. a single square will be a member 36 | // of a row unit, a column unit and a 3x3 block unit. 37 | type Unit = []Index 38 | 39 | // Values represents a Sudoku board in a format that's usable for solving. 40 | // An element at index [i] in Values represents Sudoku square i (see the 41 | // documentation of the Index type), and contains a set of all candidate 42 | // digits for this square. 43 | type Values []Digits 44 | 45 | // unitlist is the list of all units that exist on the board. 46 | var unitlist []Unit 47 | 48 | // units maps an index to a list of units that contain that square. 49 | // The mapping is a slice, i.e. units[i] is a list of all the units 50 | // that contain the square with index i. 51 | var units [][]Unit 52 | 53 | // peers maps an index to a list of unique peers - other indices that share 54 | // some unit with this index (it won't contain the index itself). 55 | var peers [][]Index 56 | 57 | func init() { 58 | index := func(row, col int) Index { 59 | return row*9 + col 60 | } 61 | 62 | // row units 63 | for row := 0; row < 9; row++ { 64 | var rowUnit []Index 65 | for col := 0; col < 9; col++ { 66 | rowUnit = append(rowUnit, index(row, col)) 67 | } 68 | unitlist = append(unitlist, rowUnit) 69 | } 70 | 71 | // column units 72 | for col := 0; col < 9; col++ { 73 | var colUnit []Index 74 | for row := 0; row < 9; row++ { 75 | colUnit = append(colUnit, index(row, col)) 76 | } 77 | unitlist = append(unitlist, colUnit) 78 | } 79 | 80 | // 3x3 block units 81 | for blockRow := 0; blockRow < 3; blockRow++ { 82 | for blockCol := 0; blockCol < 3; blockCol++ { 83 | var blockUnit []Index 84 | 85 | for row := 0; row < 3; row++ { 86 | for col := 0; col < 3; col++ { 87 | blockUnit = append(blockUnit, index(blockRow*3+row, blockCol*3+col)) 88 | } 89 | } 90 | unitlist = append(unitlist, blockUnit) 91 | } 92 | } 93 | 94 | // For each index i, units[i] is a list of all units that contain i. 95 | units = make([][]Unit, 81) 96 | for i := 0; i < 81; i++ { 97 | for _, unit := range unitlist { 98 | if slices.Index(unit, i) >= 0 { 99 | units[i] = append(units[i], slices.Clone(unit)) 100 | } 101 | } 102 | } 103 | 104 | // For each index i, peers[i] is a list of unique indices that share some 105 | // unit with i. 106 | peers = make([][]Index, 81) 107 | for i := 0; i < 81; i++ { 108 | for _, unit := range units[i] { 109 | for _, candidate := range unit { 110 | // This uses linear search to ensure uniqueness, but this calculation is 111 | // only done once at solver creation so we don't particularly care about 112 | // its speed. 113 | if candidate != i && slices.Index(peers[i], candidate) < 0 { 114 | peers[i] = append(peers[i], candidate) 115 | } 116 | } 117 | } 118 | } 119 | } 120 | 121 | // ParseBoard parses a Sudoku board given in textual representation, and returns 122 | // it as Values. The textual representation is as described in 123 | // http://norvig.com/sudoku.html: a string with a sequence of 81 runes in the 124 | // set [0123456789.], where 0 or . mean "unassigned". All other runes in the 125 | // string are ignored, making the input format flexible (it can be just 81 126 | // runes on a single line, or it can be a nice multi-line |-separated board 127 | // representation - since all runes outside the aforementioned set are ignored). 128 | // 129 | // If runElimination is false, the board is returned immediately after parsing. 130 | // If runElimination is true, ParseBoard will invoke EliminateAll on the board 131 | // and return the result. This is recommended when the board is then passed to 132 | // a solver. 133 | // It returns an error if there was an issue parsing the board, or if the board 134 | // isn't a valid Sudoku board (e.g. contradictions exist). 135 | func ParseBoard(str string, runElimination bool) (Values, error) { 136 | var dgs []uint16 137 | 138 | // Iterate and grab only the supported runes; ignore all others. 139 | for _, r := range str { 140 | if r >= '0' && r <= '9' { 141 | dgs = append(dgs, uint16(r)-uint16('0')) 142 | } else if r == '.' { 143 | dgs = append(dgs, 0) 144 | } 145 | } 146 | 147 | if len(dgs) != 81 { 148 | return nil, fmt.Errorf("got only %v digits in board, want 81", len(dgs)) 149 | } 150 | 151 | // Start with an empty board. 152 | values := EmptyBoard() 153 | 154 | // Assign square digits based on the parsed board. Note that this runs 155 | // constraint propagation and may discover contradictions. 156 | for sq, d := range dgs { 157 | if d != 0 { 158 | values[sq] = SingleDigitSet(d) 159 | } 160 | } 161 | 162 | if runElimination && !EliminateAll(values) { 163 | return nil, fmt.Errorf("contradiction when eliminating board") 164 | } 165 | 166 | return values, nil 167 | } 168 | 169 | // EliminateAll runs elimination on all assigned squares in values. It applies 170 | // first-order Sudoku heuristics on the entire board. Returns true if the 171 | // elimination is successful, and false if the board has a contradiction. 172 | func EliminateAll(values Values) bool { 173 | for sq, d := range values { 174 | if d.Size() == 1 { 175 | // Because of how eliminate() works, we prepare for it by remembering 176 | // which digit this square has assigned, setting the square to the full 177 | // set of digits and then calling eliminate on all digits except the 178 | // assigned one. 179 | digit := d.SingleMemberDigit() 180 | values[sq] = FullDigitsSet() 181 | for dn := uint16(1); dn <= 9; dn++ { 182 | if dn != digit { 183 | if !eliminate(values, sq, dn) { 184 | return false 185 | } 186 | } 187 | } 188 | } 189 | } 190 | return true 191 | } 192 | 193 | // CountHints counts the total number of hints on the board. 194 | func CountHints(values Values) int { 195 | hintcount := 0 196 | for _, d := range values { 197 | if d.Size() == 1 { 198 | hintcount++ 199 | } 200 | } 201 | return hintcount 202 | } 203 | 204 | // assign attempts to assign digit to values[square], propagating 205 | // constraints from the assignment. values is modified. 206 | // It returns true if the assignment succeeded, and false if the assignment 207 | // fails resulting in an invalid Sudoku board. 208 | func assign(values Values, square Index, digit uint16) bool { 209 | if EnableStats { 210 | Stats.NumAssigns++ 211 | } 212 | 213 | for d := uint16(1); d <= 9; d++ { 214 | // For each d 1..9 that's != digit, if d is set in 215 | // values[square], try to eliminate it. 216 | if values[square].IsMember(d) && d != digit { 217 | if !eliminate(values, square, d) { 218 | return false 219 | } 220 | } 221 | } 222 | return true 223 | } 224 | 225 | // eliminate removes digit from the candidates in values[square], propagating 226 | // constraints. values is modified. 227 | // It returns false if this results in an invalid Sudoku board; otherwise 228 | // returns true. 229 | func eliminate(values Values, square Index, digit uint16) bool { 230 | if !values[square].IsMember(digit) { 231 | // Already eliminated 232 | return true 233 | } 234 | 235 | // Remove digit from the candidates in square. 236 | values[square] = values[square].Remove(digit) 237 | 238 | switch values[square].Size() { 239 | case 0: 240 | // No remaining options for square -- this is a contradiction. 241 | return false 242 | case 1: 243 | // A single digit candidate remaining in the square -- this creates a new 244 | // constraint. Eliminate this digit from all peer squares. 245 | remaining := values[square].SingleMemberDigit() 246 | for _, peer := range peers[square] { 247 | if !eliminate(values, peer, remaining) { 248 | return false 249 | } 250 | } 251 | } 252 | 253 | // Since digit was eliminated from square, it's possible that we'll find a 254 | // position for this digit in one of the units the square belongs to. 255 | UnitLoop: 256 | for _, unit := range units[square] { 257 | // Looking for a single square in this unit that has 'digit' as one of its 258 | // candidates. sqd marks the square, or -1 if no such square was found. 259 | sqd := -1 260 | for _, sq := range unit { 261 | if values[sq].IsMember(digit) { 262 | if sqd == -1 { 263 | sqd = sq 264 | } else { 265 | // More than one square has 'digit' as a candidate, so we won't be 266 | // able to simplify things. 267 | continue UnitLoop 268 | } 269 | } 270 | } 271 | if sqd == -1 { 272 | // Contradiction: no places left in this unit for 'digit' 273 | return false 274 | } 275 | 276 | // There's only a single place left in the unit for 'digit' to go, so 277 | // assign it. 278 | if !assign(values, sqd, digit) { 279 | return false 280 | } 281 | } 282 | 283 | return true 284 | } 285 | 286 | // Display returns a visual representation of values, with all the digit 287 | // candidates as a string in each cell. 288 | func Display(values Values) string { 289 | // Find maximum length of one square. 290 | var maxlen int = 0 291 | for _, d := range values { 292 | if d.Size() > maxlen { 293 | maxlen = d.Size() 294 | } 295 | } 296 | width := maxlen + 1 297 | 298 | line := strings.Join([]string{ 299 | strings.Repeat("-", width*3), 300 | strings.Repeat("-", width*3), 301 | strings.Repeat("-", width*3)}, "+") 302 | 303 | var sb strings.Builder 304 | for sq, d := range values { 305 | fmt.Fprintf(&sb, "%[1]*s", -width, fmt.Sprintf("%[1]*s", (width+d.Size())/2, d)) 306 | if sq%9 == 2 || sq%9 == 5 { 307 | sb.WriteString("|") 308 | } 309 | if sq%9 == 8 { 310 | sb.WriteRune('\n') 311 | } 312 | if sq == 26 || sq == 53 { 313 | sb.WriteString(line + "\n") 314 | } 315 | } 316 | return sb.String() 317 | } 318 | 319 | // DisplayAsInput returns a 2D Sudoku input board corresponding to values. 320 | // It treats solved squares (with one candidate) as hints that are filled into 321 | // the board, and unsolved squares (with more than one candidate) as empty. 322 | func DisplayAsInput(values Values) string { 323 | line := strings.Join([]string{ 324 | strings.Repeat("-", 6), 325 | strings.Repeat("-", 6), 326 | strings.Repeat("-", 6)}, "+") 327 | 328 | var sb strings.Builder 329 | for sq, d := range values { 330 | ds := d.String() 331 | if d.Size() > 1 { 332 | ds = "." 333 | } 334 | fmt.Fprintf(&sb, "%s ", ds) 335 | if sq%9 == 2 || sq%9 == 5 { 336 | sb.WriteString("|") 337 | } 338 | if sq%9 == 8 { 339 | sb.WriteRune('\n') 340 | } 341 | if sq == 26 || sq == 53 { 342 | sb.WriteString(line + "\n") 343 | } 344 | } 345 | return sb.String() 346 | } 347 | 348 | // DisplayAsSVG write the board's visual representation in SVG format into w. 349 | // The difficulty is emitted too. 350 | func DisplayAsSVG(w io.Writer, values Values, difficulty float64) { 351 | startX := 50 352 | startY := 50 353 | width := 800 354 | height := 900 355 | cellsize := 80 356 | canvas := svg.New(w, width, height) 357 | 358 | for sq, d := range values { 359 | col := sq % 9 360 | x := startX + col*cellsize 361 | 362 | row := sq / 9 363 | y := startY + row*cellsize 364 | 365 | canvas.Rect(x, y, cellsize, cellsize, "stroke:black; stroke-width:2; fill:white") 366 | if d.Size() == 1 { 367 | canvas.Text(x+cellsize/2, y+cellsize/2, d.String(), "text-anchor:middle; dominant-baseline:middle; font-family:Helvetica; font-size:32px; fill:black") 368 | } 369 | } 370 | 371 | // Wider squares around 3x3 blocks 372 | for br := 0; br < 3; br++ { 373 | for bc := 0; bc < 3; bc++ { 374 | canvas.Rect(startX+bc*cellsize*3, startY+br*cellsize*3, cellsize*3, cellsize*3, "stroke:black; stroke-width:5; fill-opacity:0.0") 375 | } 376 | } 377 | 378 | difficultyText := fmt.Sprintf("Difficulty: %.2f out of 5", difficulty) 379 | canvas.Text(startX, startY+9*cellsize+cellsize/2, difficultyText, "font-family:Helvetica; font-size:16px; fill:black") 380 | 381 | canvas.End() 382 | } 383 | 384 | // EmptyBoard creates an "empty" Sudoku board, where each square can potentially 385 | // contain any digit. 386 | func EmptyBoard() Values { 387 | vals := make(Values, 81) 388 | for sq := range vals { 389 | vals[sq] = FullDigitsSet() 390 | } 391 | return vals 392 | } 393 | 394 | // IsSolved checks whether values is a properly solved Sudoku board, with all 395 | // the constraints satisfied. 396 | func IsSolved(values Values) bool { 397 | for _, unit := range unitlist { 398 | var dset Digits 399 | for _, sq := range unit { 400 | // Some squares have more than a single candidate? Not solved. 401 | if values[sq].Size() != 1 { 402 | return false 403 | } 404 | dset = dset.Add(values[sq].SingleMemberDigit()) 405 | } 406 | // Not all digits covered by this unit? Not solved. 407 | if dset != FullDigitsSet() { 408 | return false 409 | } 410 | } 411 | return true 412 | } 413 | 414 | // findSquareWithFewestCandidates finds a square in values with more than one 415 | // digit candidate, but the smallest number of such candidates. 416 | func findSquareWithFewestCandidates(values Values) Index { 417 | var squareToTry Index = -1 418 | var minSize int = 10 419 | for sq, d := range values { 420 | if d.Size() > 1 && d.Size() < minSize { 421 | minSize = d.Size() 422 | squareToTry = sq 423 | } 424 | } 425 | return squareToTry 426 | } 427 | 428 | // SolveOptions is a container of options for the Solve function. 429 | type SolveOptions struct { 430 | // Randomize tells the solver to randomly shuffle its digit selection when 431 | // attempting to guess a value for a square. For actual randomness, the 432 | // rand package's default randomness source should be properly seeded before 433 | // invoking Solve. 434 | Randomize bool 435 | } 436 | 437 | // Solve runs a backtracking search to solve the board given in values. 438 | // It returns true and the solved values if the search succeeded and we ended up 439 | // with a board with only a single candidate per square; otherwise, it returns 440 | // false. The input values is not modified. 441 | // The solution process can be configured by providing SolveOptions. 442 | func Solve(values Values, options ...SolveOptions) (Values, bool) { 443 | if len(options) > 1 { 444 | panic("Solve cannot accept more than a single SolveOptions") 445 | } 446 | 447 | squareToTry := findSquareWithFewestCandidates(values) 448 | 449 | // If we didn't find any square with more than one candidate, the board is 450 | // solved! 451 | if squareToTry == -1 { 452 | return values, true 453 | } 454 | 455 | if EnableStats { 456 | Stats.NumSearches++ 457 | } 458 | 459 | var candidates = []uint16{1, 2, 3, 4, 5, 6, 7, 8, 9} 460 | if len(options) > 0 && options[0].Randomize { 461 | rand.Shuffle(len(candidates), func(i, j int) { 462 | candidates[i], candidates[j] = candidates[j], candidates[i] 463 | }) 464 | } 465 | 466 | for _, d := range candidates { 467 | // Try to assign sq with each one of its candidate digits. If this results 468 | // in a successful Solve() - we've solved the board! 469 | if values[squareToTry].IsMember(d) { 470 | vcopy := slices.Clone(values) 471 | if assign(vcopy, squareToTry, d) { 472 | if vresult, solved := Solve(vcopy, options...); solved { 473 | return vresult, true 474 | } 475 | } 476 | } 477 | } 478 | return values, false 479 | } 480 | 481 | // SolveAll finds all solutions to the given board and returns them. If no 482 | // solutions were found, an empty list is returned. max can specify the 483 | // (approximate) maximal number of solutions to find; a value <= 0 means "all of 484 | // them". Often more solutions than max will be returned, but not a lot more 485 | // (maybe 2-3x as many). values is not modified. 486 | // Warning: this function can take a LONG time to run for boards with multiple 487 | // solutions, and it can consume enormous amounts of memory because it has to 488 | // remember each solution it finds. For some boards it will run forever (e.g. 489 | // finding all solutions on an empty board). If in doubt, use the max parameter 490 | // to restrict the number. 491 | func SolveAll(values Values, max int) []Values { 492 | squareToTry := findSquareWithFewestCandidates(values) 493 | 494 | // If we didn't find any square with more than one candidate, the board is 495 | // solved! 496 | if squareToTry == -1 { 497 | return []Values{values} 498 | } 499 | 500 | var allSolved []Values 501 | 502 | for d := uint16(1); d <= 9; d++ { 503 | // Try to assign sq with each one of its candidate digits. If this results 504 | // in a successful Solve() - we've solved the board! 505 | if values[squareToTry].IsMember(d) { 506 | vcopy := slices.Clone(values) 507 | if assign(vcopy, squareToTry, d) { 508 | if vsolved := SolveAll(vcopy, max); len(vsolved) > 0 { 509 | allSolved = append(allSolved, vsolved...) 510 | if max > 0 && len(allSolved) >= max { 511 | return allSolved 512 | } 513 | } 514 | } 515 | } 516 | } 517 | return allSolved 518 | } 519 | 520 | // EnableStats enables statistics collection during the processes of solving. 521 | // When stats are enabled, solving will be slightly slower. 522 | // 523 | // Note: statistics collection is NOT SAFE FOR CONCURRENT ACCESS. 524 | var EnableStats bool = false 525 | 526 | type StatsCollector struct { 527 | NumSearches uint64 528 | NumAssigns uint64 529 | } 530 | 531 | // Stats is the global variable for accessing statistics from this package. 532 | // It's recommended to call Stats.Reset() before solving a board, and access 533 | // the Stats fields after it's done. 534 | var Stats StatsCollector 535 | 536 | func (s *StatsCollector) Reset() { 537 | s.NumSearches = 0 538 | s.NumAssigns = 0 539 | } 540 | 541 | // WithStats helps run any block of code with stats enabled. 542 | func WithStats(f func()) { 543 | EnableStats = true 544 | defer func() { 545 | EnableStats = false 546 | }() 547 | Stats.Reset() 548 | 549 | f() 550 | } 551 | 552 | // ApplyTwinsStrategy applies the "naked twins" Sudoku strategy to the given 553 | // board and updates it. It returns false if there was a contradiction 554 | // discovered while applying the strategy. 555 | // The "naked twins" strategy is to find two digits that are the only candidates 556 | // in two squares in the same unit. This helps eliminate these digits from 557 | // other squares in the same unit. 558 | // For example, two squares in the same row cell may have 38 as their only 559 | // candidates, which means that 3 and 8 must occupy these squares (though we 560 | // don't know which goes where), and that no other square in the unit may have 561 | // either 3 or 8. 562 | func ApplyTwinsStrategy(values Values) bool { 563 | // The strategy is repeated to a "fixed point" where further runs don't end 564 | // up changing the board in any way. 565 | RepeatStrategy: 566 | for { 567 | for _, unit := range unitlist { 568 | // dcount will map Digits->count, counting how many times a certain 569 | // combination of digit candidates appears in this unit. 570 | dcount := make(map[Digits]int) 571 | for _, sq := range unit { 572 | dcount[values[sq]]++ 573 | } 574 | 575 | var removed bool 576 | for d, count := range dcount { 577 | if d.Size() == 2 && count == 2 { 578 | // Found it! Now go over all the squares in this unit other than the 579 | // ones with these exact two candidates, and eliminate both candidates 580 | // from them. 581 | d1, d2 := d.twoMemberDigits() 582 | for _, sq := range unit { 583 | if values[sq].Size() >= 2 && values[sq] != d { 584 | if values[sq].IsMember(d1) { 585 | if !eliminate(values, sq, d1) { 586 | return false 587 | } 588 | removed = true 589 | } 590 | if values[sq].IsMember(d2) { 591 | if !eliminate(values, sq, d2) { 592 | return false 593 | } 594 | removed = true 595 | } 596 | } 597 | } 598 | } 599 | // We've eliminated some values from the board, so it's not safe to 600 | // proceed with this loop since dcount may hold stale values. We 601 | // instead repeat the strategy on the whole board. 602 | if removed { 603 | continue RepeatStrategy 604 | } 605 | } 606 | } 607 | 608 | // We went over all units and didn't remove anything in this round, so 609 | // no point repeating. 610 | return true 611 | } 612 | } 613 | -------------------------------------------------------------------------------- /sudoku_fuzz_test.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func FuzzParseboard(f *testing.F) { 9 | f.Add("4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......") 10 | f.Add(DisplayAsInput(EmptyBoard())) 11 | f.Add(` 12 | . . . |. . 6 |. . . 13 | . 5 9 |. . . |. . 8 14 | 2 . . |. . 8 |. . . 15 | ------+------+------ 16 | . 4 5 |. . . |. . . 17 | . . 3 |. . . |. . . 18 | . . 6 |. . 3 |. 5 4 19 | ------+------+------ 20 | . . . |3 2 5 |. . 6 21 | . . . |. . . |. . . 22 | . . . |. . . |. . .`) 23 | f.Add("1234") 24 | f.Add("") 25 | f.Add(strings.Repeat("1", 81)) 26 | 27 | f.Fuzz(func(t *testing.T, bstr string) { 28 | // Mostly checks that there are no panics, and ParseBoard returns either 29 | // a board or and error, but not both. 30 | b, err := ParseBoard(bstr, true) 31 | if b != nil && err != nil { 32 | t.Fatalf("expect b or err to be nil, got b=%v, err=%v", b, err) 33 | } 34 | if b == nil && err == nil { 35 | t.Fatalf("expect b or err to be non-nil, got b=%v, err=%v", b, err) 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /sudoku_test.go: -------------------------------------------------------------------------------- 1 | package sudoku 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "testing" 8 | 9 | "slices" 10 | ) 11 | 12 | func TestInit(t *testing.T) { 13 | // Smoke testing for the top-level vars initialized in init() 14 | if len(unitlist) != 27 { 15 | t.Errorf("got len=%v, want 27", len(unitlist)) 16 | } 17 | 18 | wantUnits := []Unit{ 19 | {18, 19, 20, 21, 22, 23, 24, 25, 26}, 20 | {2, 11, 20, 29, 38, 47, 56, 65, 74}, 21 | {0, 1, 2, 9, 10, 11, 18, 19, 20}} 22 | 23 | if !slices.EqualFunc(wantUnits, units[20], func(a, b Unit) bool { 24 | return slices.Equal(a, b) 25 | }) { 26 | t.Errorf("got units[20]=%v\nwant %v", units[20], wantUnits) 27 | } 28 | 29 | gotPeers := peers[20] 30 | slices.Sort(gotPeers) 31 | wantPeers := []Index{0, 1, 2, 9, 10, 11, 18, 19, 21, 22, 23, 24, 25, 26, 29, 38, 47, 56, 65, 74} 32 | if !slices.Equal(wantPeers, gotPeers) { 33 | t.Errorf("got peers[20]=%v\n want %v", peers[20], wantPeers) 34 | } 35 | } 36 | 37 | func TestAssignElimination(t *testing.T) { 38 | vals := EmptyBoard() 39 | 40 | if IsSolved(vals) { 41 | t.Errorf("an empty board is solved") 42 | } 43 | 44 | // Assign a digit to square 20; check that this digit is the only candidate 45 | // in square 20, and that it was eliminated from all the peers of 20. 46 | assign(vals, 20, 5) 47 | 48 | if vals[20].Size() != 1 || vals[20].SingleMemberDigit() != 5 { 49 | t.Errorf("got vals[20]=%v", vals[20]) 50 | } 51 | 52 | for sq := 0; sq <= 80; sq++ { 53 | if slices.Contains(peers[20], sq) { 54 | if vals[sq].IsMember(5) { 55 | t.Errorf("got member 5 in peer square %v", sq) 56 | } 57 | } else { 58 | if !vals[sq].IsMember(5) { 59 | t.Errorf("got no member 5 in non-peer square %v", sq) 60 | } 61 | } 62 | } 63 | } 64 | 65 | // Easy board from Norvig's example that's solved by constraint propagation 66 | // w/o any search. 67 | var easyboard1 string = "003020600900305001001806400008102900700000008006708200002609500800203009005010300" 68 | 69 | // Hard board from Norvig's example. 70 | var hardboard1 string = "4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......" 71 | var hardboard2 string = "..53.....8......2..7..1.5..4....53...1..7...6..32...8..6.5....9..4....3......97.." 72 | 73 | // This is the program-generated sudoku board Norvig reported takes the longest 74 | // for his solver to crack. Note that this board has a very large number of 75 | // solutions. 76 | var hardlong string = ` 77 | . . . |. . 6 |. . . 78 | . 5 9 |. . . |. . 8 79 | 2 . . |. . 8 |. . . 80 | ------+------+------ 81 | . 4 5 |. . . |. . . 82 | . . 3 |. . . |. . . 83 | . . 6 |. . 3 |. 5 4 84 | ------+------+------ 85 | . . . |3 2 5 |. . 6 86 | . . . |. . . |. . . 87 | . . . |. . . |. . .` 88 | 89 | func TestParseBoard(t *testing.T) { 90 | // Need to test the "false" variant of ParseBoard too 91 | v, err := ParseBoard(easyboard1, true) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | if !IsSolved(v) { 97 | t.Errorf("expect easy board to be solved w/ elimination") 98 | } 99 | 100 | // Without runElimination, the easy board will not be immediately solved by 101 | // parsing. 102 | ve, err := ParseBoard(easyboard1, false) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | 107 | if IsSolved(ve) { 108 | t.Errorf("expect easy board to not be solved w/o elimination") 109 | } 110 | 111 | if !EliminateAll(ve) || !IsSolved(ve) { 112 | t.Errorf("expect easy board to be solved with explicit elimination") 113 | } 114 | 115 | // Harder board that isn't fully solved without search. 116 | v2, err := ParseBoard(hardboard1, true) 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | 121 | if IsSolved(v2) { 122 | t.Errorf("expect hard board to not be solved") 123 | } 124 | 125 | // Count how many squares are solved immediately in this puzzle and compare 126 | // to the number Norvig got. 127 | var solvedSquares int 128 | for _, d := range v2 { 129 | if d.Size() == 1 { 130 | solvedSquares++ 131 | } 132 | } 133 | 134 | if solvedSquares != 20 { 135 | t.Errorf("got %v solved squares, want 20", solvedSquares) 136 | } 137 | } 138 | 139 | func TestSolveBoard(t *testing.T) { 140 | v, err := ParseBoard(hardboard1, true) 141 | if err != nil { 142 | log.Fatal(err) 143 | } 144 | vcopy := slices.Clone(v) 145 | vs, success := Solve(v) 146 | if !slices.Equal(v, vcopy) { 147 | t.Errorf("Solve modified board; before=%v, after=%v", vcopy, v) 148 | } 149 | 150 | if !success || !IsSolved(vs) { 151 | t.Errorf("expect hardboard1 to be solved by search") 152 | } 153 | 154 | // Should work on the easy board also (even though it's solved with the 155 | // initial parse) 156 | v2, err := ParseBoard(easyboard1, true) 157 | if err != nil { 158 | log.Fatal(err) 159 | } 160 | v2, success2 := Solve(v2) 161 | 162 | if !success2 || !IsSolved(v2) { 163 | t.Errorf("expect easy board to be solved by search") 164 | } 165 | 166 | // And the other hard board 167 | v3, err := ParseBoard(hardboard2, true) 168 | if err != nil { 169 | log.Fatal(err) 170 | } 171 | v3, success3 := Solve(v3) 172 | 173 | if !success3 || !IsSolved(v3) { 174 | t.Errorf("expect hardboard2 to be solved by search") 175 | } 176 | } 177 | 178 | func TestSolveWithStats(t *testing.T) { 179 | // The easy board is solved just by calling ParseBoard, needing no search. 180 | WithStats(func() { 181 | _, err := ParseBoard(easyboard1, true) 182 | if err != nil { 183 | t.Fatal(err) 184 | } 185 | 186 | if Stats.NumAssigns == 0 { 187 | t.Errorf("got NumAssigns==0") 188 | } 189 | if Stats.NumSearches != 0 { 190 | t.Errorf("got NumSearches=%v, want 0", Stats.NumSearches) 191 | } 192 | 193 | // For the hard board, we'll find both assigns and searches 194 | Stats.Reset() 195 | 196 | v, err := ParseBoard(hardboard1, true) 197 | if err != nil { 198 | t.Fatal(err) 199 | } 200 | _, _ = Solve(v) 201 | 202 | if Stats.NumAssigns == 0 { 203 | t.Errorf("got NumAssigns==0") 204 | } 205 | if Stats.NumSearches == 0 { 206 | t.Errorf("got NumSearches==0") 207 | } 208 | }) 209 | } 210 | 211 | func TestIsSolved(t *testing.T) { 212 | v, err := ParseBoard(easyboard1, true) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | 217 | if !IsSolved(v) { 218 | t.Errorf("expect easy board to be solved") 219 | } 220 | 221 | // Now modify the board and make sure it's not considered "solved" any more. 222 | // ... modify by trying to add options to each square separately. 223 | for sq := range v { 224 | vcopy := slices.Clone(v) 225 | vcopy[sq] = vcopy[sq].Add(6).Add(8) 226 | 227 | if IsSolved(vcopy) { 228 | t.Errorf("expect board to not be solved after modification: %v", vcopy) 229 | } 230 | } 231 | } 232 | 233 | func TestSolveAll(t *testing.T) { 234 | v, err := ParseBoard(hardboard1, true) 235 | if err != nil { 236 | log.Fatal(err) 237 | } 238 | vcopy := slices.Clone(v) 239 | vs := SolveAll(v, -1) 240 | 241 | if len(vs) != 1 { 242 | t.Errorf("got %v solutions, want 1", len(vs)) 243 | } 244 | if !IsSolved(vs[0]) { 245 | t.Errorf("got %v, want solved board", vs[0]) 246 | } 247 | 248 | if !slices.Equal(v, vcopy) { 249 | t.Errorf("SolveAll modified its input values") 250 | } 251 | 252 | // Now generate a multiple-solution board, by replacing all instances 253 | // of 1 and 2 in the solved board by the digits set "12", permitting any 254 | // combination of them to solve the board. 255 | board := vs[0] 256 | for sq, d := range board { 257 | if d.Size() == 1 && (d.IsMember(1) || d.IsMember(2)) { 258 | board[sq] = d.Add(1).Add(2) 259 | } 260 | } 261 | 262 | vs = SolveAll(board, -1) 263 | if len(vs) != 2 { 264 | t.Errorf("got %v solved boards, want 2", len(vs)) 265 | } 266 | 267 | if !IsSolved(vs[0]) || !IsSolved(vs[1]) { 268 | t.Errorf("got unsolved boards") 269 | } 270 | 271 | // Now try to limit max=1, see that SolveAll only returns a single solution. 272 | vs = SolveAll(board, 1) 273 | if len(vs) != 1 { 274 | t.Errorf("got %v solved boards, want 1", len(vs)) 275 | } 276 | 277 | if !IsSolved(vs[0]) { 278 | t.Errorf("got unsolved boards") 279 | } 280 | 281 | // Create a board with a contradiction on purpose, and verify that SolveAll 282 | // returns an empty list. 283 | v, err = ParseBoard(hardboard1, true) 284 | if err != nil { 285 | log.Fatal(err) 286 | } 287 | v[30] = SingleDigitSet(1) 288 | v[31] = SingleDigitSet(2) 289 | v[32] = SingleDigitSet(3) 290 | vs = SolveAll(v, -1) 291 | if len(vs) != 0 { 292 | t.Errorf("expect unsolvable, got %v", vs) 293 | } 294 | } 295 | 296 | func TestHardlong(t *testing.T) { 297 | v, err := ParseBoard(hardlong, true) 298 | if err != nil { 299 | log.Fatal(err) 300 | } 301 | 302 | // The "hardlong" puzzle has multiple solutions. Norvig says he found 13 303 | // different solutions, but there are vastly more. Use SolveAll to explore 304 | // the first 1000 or so. 305 | 306 | // Find the first 1000 solutions 307 | vs := SolveAll(v, 1000) 308 | 309 | if len(vs) < 1000 { 310 | t.Errorf("got %v solutions, expected at least 1000", len(vs)) 311 | } 312 | 313 | for _, v := range vs { 314 | if !IsSolved(v) { 315 | t.Errorf("got unsolved board %v", v) 316 | } 317 | } 318 | } 319 | 320 | func TestApplyTwinsStrategy(t *testing.T) { 321 | // Basic test: on an empty board, leave only candidates 38 for two squares 322 | // and verify that twin elimination removed what was needed. 323 | 324 | v := EmptyBoard() 325 | d38 := Digits(0).Add(3).Add(8) 326 | v[30] = d38 327 | v[31] = d38 328 | 329 | ApplyTwinsStrategy(v) 330 | 331 | for _, sq := range []Index{27, 28, 39, 32, 33, 34, 35, 39, 40, 41, 48, 49, 50} { 332 | if v[sq].IsMember(3) || v[sq].IsMember(8) { 333 | t.Errorf("got board[%v]=%s, expect no 3 or 8", sq, v[sq]) 334 | } 335 | } 336 | } 337 | 338 | // This board is unsolvable, and it takes the search a while to figure this 339 | // out. 340 | var impossible string = ` 341 | . . . |. . 5 |. 8 . 342 | . . . |6 . 1 |. 4 3 343 | . . . |. . . |. . . 344 | ------+------+------ 345 | . 1 . |5 . . |. . . 346 | . . . |1 . 6 |. . . 347 | 3 . . |. . . |. . 5 348 | ------+------+------ 349 | 5 3 . |. . . |. 6 1 350 | . . . |. . . |. . 4 351 | . . . |. . . |. . .` 352 | 353 | // Run this test but skip in "not short" mode 354 | func TestImpossible(t *testing.T) { 355 | if testing.Short() { 356 | t.Skip("skipping test in short mode.") 357 | } 358 | 359 | WithStats(func() { 360 | v, err := ParseBoard(impossible, true) 361 | if err != nil { 362 | log.Fatal(err) 363 | } 364 | v, success := Solve(v) 365 | 366 | if success || IsSolved(v) { 367 | t.Errorf("got solved board for impossible") 368 | } 369 | fmt.Printf("searches=%v, assigns=%v\n", Stats.NumSearches, Stats.NumAssigns) 370 | }) 371 | } 372 | 373 | func TestSolveHardest(t *testing.T) { 374 | // The "hardest" puzzles Norvig found online (taken from 375 | // https://norvig.com/hardest.txt) 376 | hardest := ` 377 | 85...24..72......9..4.........1.7..23.5...9...4...........8..7..17..........36.4. 378 | ..53.....8......2..7..1.5..4....53...1..7...6..32...8..6.5....9..4....3......97.. 379 | 12..4......5.69.1...9...5.........7.7...52.9..3......2.9.6...5.4..9..8.1..3...9.4 380 | ...57..3.1......2.7...234......8...4..7..4...49....6.5.42...3.....7..9....18..... 381 | 7..1523........92....3.....1....47.8.......6............9...5.6.4.9.7...8....6.1. 382 | 1....7.9..3..2...8..96..5....53..9...1..8...26....4...3......1..4......7..7...3.. 383 | 1...34.8....8..5....4.6..21.18......3..1.2..6......81.52..7.9....6..9....9.64...2 384 | ...92......68.3...19..7...623..4.1....1...7....8.3..297...8..91...5.72......64... 385 | .6.5.4.3.1...9...8.........9...5...6.4.6.2.7.7...4...5.........4...8...1.5.2.3.4. 386 | 7.....4...2..7..8...3..8.799..5..3...6..2..9...1.97..6...3..9...3..4..6...9..1.35 387 | ....7..2.8.......6.1.2.5...9.54....8.........3....85.1...3.2.8.4.......9.7..6.... 388 | ` 389 | for _, board := range strings.Split(hardest, "\n") { 390 | board = strings.TrimSpace(board) 391 | if len(board) > 0 { 392 | v, err := ParseBoard(board, true) 393 | if err != nil { 394 | log.Fatalf("error for board %v: %v", board, err) 395 | } 396 | 397 | // Use ApplyTwinsStrategy here for correctness testing 398 | if !ApplyTwinsStrategy(v) { 399 | t.Errorf("ApplyTwinsStrategy returns contradiction") 400 | } 401 | 402 | vs, success := Solve(v) 403 | if !success || !IsSolved(vs) { 404 | t.Errorf("not solved board %v", board) 405 | } 406 | 407 | // Now solve again, with randomization 408 | vsr, success := Solve(v, SolveOptions{Randomize: true}) 409 | if !success || !IsSolved(vsr) { 410 | t.Errorf("not solved randomized board %v", board) 411 | } 412 | } 413 | } 414 | } 415 | 416 | func TestSolveEmpty(t *testing.T) { 417 | vals := EmptyBoard() 418 | vres, solved := Solve(vals) 419 | if !solved { 420 | t.Errorf("want Solve(empty) to report success") 421 | } 422 | 423 | if !IsSolved(vres) { 424 | t.Errorf("want solved result board; got:\n%v", Display(vres)) 425 | } 426 | 427 | // Try a few randomized solutions 428 | for i := 0; i < 10; i++ { 429 | vs, solved := Solve(vals, SolveOptions{Randomize: true}) 430 | if !solved { 431 | t.Errorf("want Solve(empty) to report success") 432 | } 433 | if !IsSolved(vs) { 434 | t.Errorf("want solved result board; got:\n%v", Display(vs)) 435 | } 436 | } 437 | } 438 | 439 | func BenchmarkParseBoardAssign(b *testing.B) { 440 | // Benchmark how long it takes to parse a board and run full constraint 441 | // propagation. We know that for easyboard1 it's fully solved with 442 | // constraint propagation after parsing. 443 | for i := 0; i < b.N; i++ { 444 | _, _ = ParseBoard(easyboard1, true) 445 | } 446 | } 447 | 448 | func BenchmarkSolveBoardHardlong(b *testing.B) { 449 | for i := 0; i < b.N; i++ { 450 | v, err := ParseBoard(hardlong, true) 451 | if err != nil { 452 | log.Fatal(err) 453 | } 454 | _, success := Solve(v) 455 | if !success { 456 | log.Fatal("not solved") 457 | } 458 | } 459 | } 460 | 461 | func BenchmarkSolveBoardHardlongRandomized(b *testing.B) { 462 | for i := 0; i < b.N; i++ { 463 | v, err := ParseBoard(hardlong, true) 464 | if err != nil { 465 | log.Fatal(err) 466 | } 467 | _, success := Solve(v, SolveOptions{Randomize: true}) 468 | if !success { 469 | log.Fatal("not solved") 470 | } 471 | } 472 | } 473 | 474 | func BenchmarkSolveEmpty(b *testing.B) { 475 | // Benchmark how long it takes to "solve" an empty board. 476 | empty := EmptyBoard() 477 | for i := 0; i < b.N; i++ { 478 | _, _ = Solve(empty) 479 | } 480 | } 481 | 482 | func BenchmarkApplyTwinsStrategy(b *testing.B) { 483 | for i := 0; i < b.N; i++ { 484 | v := EmptyBoard() 485 | d38 := Digits(0).Add(3).Add(8) 486 | v[30] = d38 487 | v[31] = d38 488 | 489 | ApplyTwinsStrategy(v) 490 | } 491 | } 492 | 493 | func BenchmarkSolveEmptyRandomized(b *testing.B) { 494 | // Benchmark how long it takes to "solve" an empty board, 495 | // with randomization. Each solution will be different. 496 | empty := EmptyBoard() 497 | for i := 0; i < b.N; i++ { 498 | _, _ = Solve(empty) 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /svg/svg.go: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "io" 7 | "text/template" 8 | ) 9 | 10 | type Canvas struct { 11 | writer io.Writer 12 | } 13 | 14 | var headerText = ` 15 | 18 | ` 19 | 20 | var headerTemplate = template.Must(template.New("header").Parse(headerText)) 21 | 22 | func New(writer io.Writer, width, height int) *Canvas { 23 | c := &Canvas{writer: writer} 24 | 25 | headerTemplate.Execute(writer, struct { 26 | Width, Height int 27 | }{Width: width, Height: height}) 28 | 29 | return c 30 | } 31 | 32 | func (c *Canvas) End() { 33 | fmt.Fprintf(c.writer, "\n") 34 | } 35 | 36 | func (c *Canvas) Rect(x, y, width, height int, style string) { 37 | fmt.Fprintf(c.writer, ` 0 { 39 | fmt.Fprintf(c.writer, ` style="%s"`, style) 40 | } 41 | fmt.Fprintf(c.writer, "/>\n") 42 | } 43 | 44 | func (c *Canvas) Text(x, y int, text string, style string) { 45 | fmt.Fprintf(c.writer, ` 0 { 47 | fmt.Fprintf(c.writer, ` style="%s"`, style) 48 | } 49 | fmt.Fprintf(c.writer, ">") 50 | xml.Escape(c.writer, []byte(text)) 51 | fmt.Fprintf(c.writer, "\n") 52 | } 53 | -------------------------------------------------------------------------------- /svg/svg_test.go: -------------------------------------------------------------------------------- 1 | package svg 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // Sanity testing. 10 | func TestSvgWrite(t *testing.T) { 11 | var buf bytes.Buffer 12 | 13 | width := 120 14 | height := 233 15 | x := 77 16 | y := 88 17 | 18 | canvas := New(&buf, width, height) 19 | canvas.Rect(x, y, 100, 200, "my style") 20 | canvas.Text(x+10, y+1, "hello", "") 21 | canvas.End() 22 | 23 | result := buf.String() 24 | 25 | want := ` 26 | 29 | 30 | hello 31 | ` 32 | 33 | if strings.TrimSpace(result) != strings.TrimSpace(want) { 34 | t.Errorf("got:\n %s\n\nwant:\n %v\n", result, want) 35 | } 36 | } 37 | --------------------------------------------------------------------------------