Compare commits

38 Commits

Author SHA1 Message Date
c1f2a28ac1 Provided tests for functions that can be tested (Closes #20). 2025-01-29 17:45:08 +01:00
cd54fdf345 Update the README.md 2025-01-28 21:09:46 +01:00
cced4f04b2 Allow to set verbosity of output (Closes #26) 2025-01-28 21:02:12 +01:00
2bdcdcb1dd Add JSON render + small refactoring (Closes #25) 2025-01-28 19:50:42 +01:00
71fefd760e Rework how priting of solutions are handled. 2025-01-28 18:29:12 +01:00
2461f5b417 Forgot a function. 2025-01-28 18:26:44 +01:00
b83d6bdde4 Give each function its own file. 2025-01-28 18:12:03 +01:00
a314e945ed Change int64 for select variables to uint64. (Closes #21) 2025-01-28 02:30:25 +01:00
2735074515 Use strings instead of ints to store blocks (Closes #23) 2025-01-28 01:59:36 +01:00
647476f6d8 Change internal Solution storage to [][]string (Closes #24) 2025-01-28 01:43:47 +01:00
515034a64e Add the ability to select type of output. 2025-01-28 01:04:38 +01:00
a7c5845b7a More comments. 2025-01-28 00:33:45 +01:00
6dd5d2f232 Make godoc happy, and add extra comments. 2025-01-28 00:31:02 +01:00
6b0e55e3f9 Change ordering of interface initialisations. 2025-01-28 00:13:16 +01:00
719dd43307 Move parts of solver into its own package. Closes #19
Reviewed-on: #22
2025-01-28 00:07:06 +01:00
6241c0b720 Remove unused restraint. 2025-01-27 20:23:48 +01:00
3d4729bdb6 Small optimization 2025-01-27 20:21:40 +01:00
7084a62962 Ignore the extra second when we are done. 2025-01-27 20:17:09 +01:00
8d2cca657b Nicely print the solution, with table borders and stuff. 2025-01-27 20:16:56 +01:00
56ebe616ab Change functions and vars from snake_case to camelCase (Closes #16) 2025-01-27 19:40:24 +01:00
fc555fa581 Use stdlib time functions to print time estimation (Closes #18) 2025-01-27 19:20:49 +01:00
0e3aa42b78 #11 It works. 2025-01-27 18:41:33 +01:00
c72bc6b0e5 Start of godoc comments. Closes #5 2025-01-27 18:41:15 +01:00
c73c88679a #13 Clean functions 2025-01-27 18:37:41 +01:00
81b44e1702 Experimenting with Gitea Actions
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 5s
2025-01-27 13:58:13 +01:00
c8f897cf8d Add Gource 2025-01-27 11:28:25 +01:00
c0525c2bc8 Add autoupdate and extra go checking to pre-commit steps. 2025-01-27 11:19:57 +01:00
3f6ba6e9dc #12 Update README.md 2025-01-27 00:59:29 +01:00
74f1fb63e1 Have the ability to do partial workloads.
(Read: Split searches among multiple machines.)
2025-01-27 00:13:00 +01:00
a7feb7101a precommit 2025-01-25 19:45:52 +01:00
28ecc74d7a Allows setting of cores to use (sorta fixes #8) 2025-01-25 19:45:31 +01:00
6d6db0ed28 #9 Make use of the Atomic package to keep track of solver.counter. 2025-01-25 19:08:31 +01:00
53953cf47c Fixing a leftover of #6, refactored to int64. 2025-01-25 17:54:18 +01:00
acf6ad1bb9 #7 (Should) fix end-state issue. 2025-01-25 17:32:11 +01:00
26b78420a2 #6 Improved estimation algorithm 2025-01-25 14:34:51 +01:00
582e268d56 #3 Prepare for release 2025-01-24 00:23:00 +01:00
5d02036f9e #1 Add README.md 2025-01-24 00:14:34 +01:00
6d2ac3675b Cleaning 2025-01-24 00:07:34 +01:00
54 changed files with 1648 additions and 676 deletions

19
.gitea/workflows/demo.txt Normal file
View File

@ -0,0 +1,19 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
builds

View File

@ -2,9 +2,17 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-fmt
- id: go-imports
- id: no-go-testing
- id: golangci-lint
- id: go-unit-tests

141
README.md Normal file
View File

@ -0,0 +1,141 @@
# Sudoku Funpark
Creating worlds most inefficient Sudoku solver, by trying every option, without any smart approach.
_(This was a learning project to get a better grasp of Golang. But more important; It was fun to do!.)_
## Goals
* Create the most inefficient Sudoku solver imaginable
* Be a learning experience for the Go programming language
I wrote [a blog post](https://blog.ligthert.net/posts/exploration-fun-and-process-cycles-of-sudoku/) about this.
## Features
* Worlds least efficient Sudoku solver
* Ability to assign a number of CPU cores to this task
* Split workloads among several computers
## Usage
To use the sudoku solver, run the binary with all the `-row` parameters available. Use other parameters to tune our output or CPU usage.
```
Usage of ./sudoku-funpark:
-numcpu int
Number of CPU cores to assign to this task. (default 12)
-output string
Type of output. 'human' for human readable. 'flat' for flat as stored in memory output. 'json' for JSON output. (default "human")
-part int
Process part x in n parts. Cannot be lower than 1, or higher than specified in split. (default 1)
-print string
'short': normal output;'long': normal output with timestamps; 'silent': Only print the results; (default "short")
-row1 string
1st row of the sudoku puzzle. (default "000000000")
-row2 string
2nd row of the sudoku puzzle. (default "000000000")
-row3 string
4rd row of the sudoku puzzle. (default "000000000")
-row4 string
4th row of the sudoku puzzle. (default "000000000")
-row5 string
5th row of the sudoku puzzle. (default "000000000")
-row6 string
6th row of the sudoku puzzle. (default "000000000")
-row7 string
7th row of the sudoku puzzle. (default "000000000")
-row8 string
8th row of the sudoku puzzle. (default "000000000")
-row9 string
9th row of the sudoku puzzle. (default "000000000")
-split int
Split the tasks in n parts. This depends on the availability of the first row. (default 1)
```
Instead of using the 3x3 blocks with 3x3 digits, it uses horizontal rows from top to bottom.
## Example
To see the solver in action, run the tool with the following parameters.
For a short running (~14 seconds) example:
> $ ./sudoku-funpark -row1 769104802 -row2 154800060 -row3 832700154 -row4 600900328 -row5 045328670 -row6 328670945 -row7 597410280 -row8 006283090 -row9 200590006
For a long running (~1 hours 15 minutes) example:
> $ ./sudoku-funpark -row1 769104802 -row2 154800060 -row3 002700150 -row4 600900308 -row5 045328670 -row6 328670945 -row7 597410280 -row8 006283090 -row9 200590006
The outpot (of the short running parameters) will look something like this:
```
./sudoku-funpark -row1 769104802 -row2 154800060 -row3 832700154 -row4 600900328 -row5 045328670 -row6 328670945 -row7 597410280 -row8 006283090 -row9 200590006
Loading blocks... Done! (38.957376ms)
Populating blocks... Done! (92.087174ms)
Number of (potential) solutions: 26542080
Validating solutions
Processing: 8% (2131893/26542080); Rate: 2131884/sec for 1.000028115s; Time left (est.): 11s
Processing: 16% (4292163/26542080); Rate: 2160219/sec for 1.000087826s; Time left (est.): 10s
Processing: 24% (6438334/26542080); Rate: 2146157/sec for 1.000017364s; Time left (est.): 9s
Processing: 32% (8529362/26542080); Rate: 2090965/sec for 1.000367121s; Time left (est.): 8s
Processing: 40% (10737065/26542080); Rate: 2207530/sec for 1.000072427s; Time left (est.): 7s
Processing: 48% (12958905/26542080); Rate: 2221755/sec for 1.000003187s; Time left (est.): 6s
Processing: 57% (15163877/26542080); Rate: 2204929/sec for 1.000002717s; Time left (est.): 5s
Processing: 65% (17254760/26542080); Rate: 2090742/sec for 1.00008452s; Time left (est.): 4s
Processing: 73% (19513142/26542080); Rate: 2258348/sec for 1.000071076s; Time left (est.): 3s
Processing: 82% (21795213/26542080); Rate: 2282028/sec for 1.000076024s; Time left (est.): 2s
Processing: 90% (24048891/26542080); Rate: 2253645/sec for 1.000146957s; Time left (est.): 1s
Processing: 98% (26226252/26542080); Rate: 2177215/sec for 1.000129955s; Time left (est.): 0s
Processing: 100% (26542080/26542080); Rate: 315792/sec for 1.000105149s; Time left (est.): 0s
Validated solutions (13.001683829s)
Solution #1:
╔═══════════╗
║769│154│832╢
║154│832│769╢
║832│769│154╢
╟───┼───┼───╢
║671│945│328╢
║945│328│671╢
║328│671│945╢
╟───┼───┼───╢
║597│416│283╢
║416│283│597╢
║283│597│416╢
╚═══════════╝
```
## Caveats
While this may very well solve all possible Sudoku puzzles (including the one [designed against brute force algorithms](https://en.wikipedia.org/wiki/Sudoku_solving_algorithms)), the blanks in the puzzle, the harder it is, the more possible solutions there are, the more solutions it needs to parse, the longer it takes. As this is a computational heavy program, the more CPU you throw against it the faster it will solve issues.
## Generating your own blocks
To generate your own blocks, you could use the following code:
```go
func (solver *Solver) generate_blocks() []int {
var blocks []int
decvals := [9]int{49, 50, 51, 52, 53, 54, 55, 56, 57}
for counter := 123456789; counter <= 987654321; counter++ {
// Convert number to string ([]byte)
digits := strconv.Itoa(counter)
// Check if every number is only represented only once
var valid bool
valid = true
for decval := range decvals {
var count int
for digit := range digits {
if digits[digit] == byte(decvals[decval]) {
count = count + 1
}
}
if count != 1 {
valid = false
}
}
if valid {
blocks = append(blocks, counter)
}
}
return blocks
}
```

View File

@ -2,6 +2,10 @@
version: '3'
vars:
APP: sudoku-funpark
BUILD_DIR: builds
tasks:
default:
cmds:
@ -17,9 +21,24 @@ tasks:
silent: true
precommit:
cmds:
- pre-commit autoupdate
- pre-commit run --all
silent: true
lint:
cmds:
- golangci-lint run
silent: true
build:
cmds:
- mkdir -p {{.BUILD_DIR}}
- rm {{.BUILD_DIR}}/* || true
- go tool dist list | grep -v android | grep -v ios | grep -v wasip1 | awk -F '/' '{printf "echo Compiling %s/%s; env CGO_ENABLED=1 GOOS=%s GOARCH=%s go build -o {{.BUILD_DIR}}/{{.APP}}.%s-%s\n",$1,$2,$1,$2,$1,$2 }' | sh
- for i in `ls {{.BUILD_DIR}}/*windows*`; do mv -v $i $i.exe; done
gource:
cmds:
- gource --auto-skip-seconds 1 --key -r 60
silent: true
godoc:
cmds:
- godoc -http=:6060
silent: true

41
controller/types.go Normal file
View File

@ -0,0 +1,41 @@
package controller
import "gitea.ligthert.net/golang/sudoku-funpark/outputter"
// Simple interface to store values shared amongst packages.
type Controller struct {
// All possible blocks/rows available
Blocks []string
// 1st row of the Sudoku puzzle.
Row1 string
// 2nd row of the Sudoku puzzle.
Row2 string
// 3rd row of the Sudoku puzzle.
Row3 string
// 4th row of the Sudoku puzzle.
Row4 string
// 5th row of the Sudoku puzzle.
Row5 string
// 6th row of the Sudoku puzzle.
Row6 string
// 7th row of the Sudoku puzzle.
Row7 string
// 8th row of the Sudoku puzzle.
Row8 string
// 9th row of the Sudoku puzzle.
Row9 string
// Slice with all found solutions.
Solutions [][]string
// Number of CPUs Go routines are allowed to use.
NumCPUs int
// Number of parts the search should be split into.
Split int
// Which part of the search should the solver focus on.
Part int
// Type of output requested
Output string
// Select printing method
Print string
// Outputter package
Outputter *outputter.Outputter
}

14
export/Export.go Normal file
View File

@ -0,0 +1,14 @@
package export
func (export *Export) Export() (render string) {
// Print the valid solutions
switch export.Controller.Output {
case "human":
render = export.renderHumanReadable()
case "flat":
render = export.renderFlat()
case "json":
render = export.renderJSON()
}
return
}

90
export/Export_test.go Normal file
View File

@ -0,0 +1,90 @@
package export
import (
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// This function will test the Export function.
// Starts off by creating all the structs and intefaces needed.
// Set all the needed values.
// Execute the Export function.
// Check if the output is a string.
// Check if the output for "human" "flat" and "json" are valid.
func TestExport(t *testing.T) {
// Create a new Export struct.
export := Export{}
// Create a new outputter struct.
outp := outputter.Outputter{}
// Set output type to "short".
outp.OutputType = "short"
// Create a new Controller struct and set the outputter.
controller := controller.Controller{Outputter: &outp}
// Set the Controller in the Export struct.
export.Controller = &controller
// Populate the Solutions slice.
controller.Solutions = append(controller.Solutions, []string{"123456789", "987654321", "123456789", "987654321", "123456789", "987654321", "123456789", "987654321", "123456789"})
// Set output type to "human".
controller.Output = "human"
// Execute the Export function.
render := export.Export()
// Check if the output is a string and not empty.
if render == "" {
t.Error("Expected a non-empty string, string was empty")
}
// Set the expected variable.
expected := "\nSolution #1:\n╔═══════════╗\n║123│456│789╢\n║987│654│321╢\n║123│456│789╢\n╟───┼───┼───╢\n║987│654│321╢\n║123│456│789╢\n║987│654│321╢\n╟───┼───┼───╢\n║123│456│789╢\n║987│654│321╢\n║123│456│789╢\n╚═══════════╝\n"
// Check if the output is the same as the expected variable.
if render != expected {
t.Error("Expected a ", expected, ", got", render)
}
// Set output type to "flat".
controller.Output = "flat"
// Execute the Export function.
render = export.Export()
// Check if the output for "flat" is non-empty.
if render == "" {
t.Error("Expected a non-empty string, string was empty")
}
expected = "\nSolution #1:\n[123456789 987654321 123456789 987654321 123456789 987654321 123456789 987654321 123456789]\n"
if render != expected {
t.Error("Expected a ", expected, ", got", render)
}
// Set output type to "json".
controller.Output = "json"
// Execute the Export function.
render = export.Export()
// Check if the output for "json" is valid.
if render == "" {
t.Error("Expected a non-empty string, got empty string")
}
// Set the expected variable.
expected = "[{\"order\":0,\"row1\":\"123456789\",\"row2\":\"987654321\",\"row3\":\"123456789\",\"row4\":\"987654321\",\"row5\":\"123456789\",\"row6\":\"987654321\",\"row7\":\"123456789\",\"row8\":\"987654321\",\"row9\":\"123456789\"}]"
// Check if what is rendered is the same as the expected variable.
if render != expected {
t.Error("Expected a ", expected, ", got", render)
}
}

15
export/renderFlat.go Normal file
View File

@ -0,0 +1,15 @@
package export
import (
"fmt"
"strconv"
)
// Render output as stored internally.
func (export *Export) renderFlat() (render string) {
for solutionIndex, solution := range export.Controller.Solutions {
render += fmt.Sprintln("\nSolution #" + strconv.Itoa(solutionIndex+1) + ":")
render += fmt.Sprintln(solution)
}
return
}

39
export/renderFlat_test.go Normal file
View File

@ -0,0 +1,39 @@
package export
import (
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// This function will test the renderFlat function.
// Then it will execute the renderFlat function.
// Check if the output is a string.
func TestRenderFlat(t *testing.T) {
// Create a new Export struct.
export := Export{}
// Create a new Controller struct.
// Set output type to "human".
outp := outputter.Outputter{}
outp.OutputType = "short"
controller := controller.Controller{Outputter: &outp}
export.Controller = &controller
controller.Solutions = append(controller.Solutions, []string{"123456789", "987654321", "123456789", "987654321", "123456789", "987654321", "123456789", "987654321", "123456789"})
// Execute the renderFlat function.
render := export.renderFlat()
// Check if the output is a string and not empty.
if render == "" {
t.Error("Expected a non-empty string, got", render)
}
// Check if the output is a string.
if render != "\nSolution #1:\n[123456789 987654321 123456789 987654321 123456789 987654321 123456789 987654321 123456789]\n" {
t.Error("Expected a string, got", render)
}
}

View File

@ -0,0 +1,22 @@
package export
import (
"fmt"
"strconv"
)
// Render solutions in a human friendly format.
func (export *Export) renderHumanReadable() (render string) {
for solutionIndex, solution := range export.Controller.Solutions {
render += fmt.Sprintln("\nSolution #" + strconv.Itoa(solutionIndex+1) + ":")
render += fmt.Sprintln("╔═══════════╗")
for rowIndex, row := range solution {
if rowIndex == 3 || rowIndex == 6 {
render += fmt.Sprintln("╟───┼───┼───╢")
}
render += fmt.Sprintln("║" + row[0:3] + "│" + row[3:6] + "│" + row[6:9] + "╢")
}
render += fmt.Sprintln("╚═══════════╝")
}
return
}

View File

@ -0,0 +1,45 @@
package export
import (
"strings"
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// This function will test the renderHumanReadable function.
// Starts off by creating a new Export struct.
// Creates a new Controller struct.
// Adds a solution to the Controller struct.
// Then it will execute the renderHumanReadable function.
// Check if the output is a string.
func TestRenderHumanReadable(t *testing.T) {
// Create a new Export struct.
export := Export{}
// Create a new Controller struct.
// Set output type to "human".
outp := outputter.Outputter{}
outp.OutputType = "short"
controller := controller.Controller{Outputter: &outp}
export.Controller = &controller
controller.Solutions = append(controller.Solutions, []string{"123456789", "987654321", "123456789", "987654321", "123456789", "987654321", "123456789", "987654321", "123456789"})
// Execute the renderHumanReadable function.
render := export.renderHumanReadable()
expected := "\nSolution #1:\n╔═══════════╗\n║123│456│789╢\n║987│654│321╢\n║123│456│789╢\n╟───┼───┼───╢\n║987│654│321╢\n║123│456│789╢\n║987│654│321╢\n╟───┼───┼───╢\n║123│456│789╢\n║987│654│321╢\n║123│456│789╢\n╚═══════════╝\n"
// Check if the output is a string and not empty.
if render == "" {
t.Error("Expected a non-empty string, got", render)
}
// Check if the output is a string.
if strings.TrimSpace(render) != strings.TrimSpace(expected) {
t.Error("Expected a string, got", render)
}
}

35
export/renderJSON.go Normal file
View File

@ -0,0 +1,35 @@
package export
import (
"encoding/json"
)
// Render JSON output.
func (export *Export) renderJSON() (render string) {
type solution_type map[string]any
solutions := make([]solution_type, 0)
for solutionIndex, solution := range export.Controller.Solutions {
solutionMap := map[string]any{
"order": solutionIndex,
"row1": solution[0],
"row2": solution[1],
"row3": solution[2],
"row4": solution[3],
"row5": solution[4],
"row6": solution[5],
"row7": solution[6],
"row8": solution[7],
"row9": solution[8],
}
solutions = append(solutions, solutionMap)
}
renderBytes, err := json.Marshal(solutions)
if err != nil {
export.Controller.Outputter.Println("ERROR: json.Marshal error:", err)
export.Controller.Outputter.Println("Printing solution as-is:", solutions)
return ""
}
render = string(renderBytes)
return
}

54
export/renderJSON_test.go Normal file
View File

@ -0,0 +1,54 @@
package export
import (
"encoding/json"
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// This function will test the renderJSON function.
// Starts off by creating a new Export struct.
// Creates a new Controller struct.
// Adds a solution to the Controller struct.
// Then it will execute the renderJSON function.
// Check if the output is a JSON string.
func TestRenderJSON(t *testing.T) {
// Create a new Export struct.
export := Export{}
outp := outputter.Outputter{}
outp.OutputType = "short"
export.Controller = &controller.Controller{Outputter: &outp}
// Create a new Controller struct.
controller := controller.Controller{}
controller.Solutions = append(controller.Solutions, []string{"123456789", "987654321", "123456789", "987654321", "123456789", "987654321", "123456789", "987654321", "123456789"})
// Add a solution to the Controller struct.
export.Controller = &controller
// Execute the renderJSON function.
render := export.renderJSON()
// Check if the output is a JSON string and not empty.
if render == "" {
t.Error("Expected a non-empty JSON string, got", render)
}
// Check if the output is a JSON string.\
if render != "[{\"order\":0,\"row1\":\"123456789\",\"row2\":\"987654321\",\"row3\":\"123456789\",\"row4\":\"987654321\",\"row5\":\"123456789\",\"row6\":\"987654321\",\"row7\":\"123456789\",\"row8\":\"987654321\",\"row9\":\"123456789\"}]" {
t.Error("Expected a JSON string, got", render)
}
// Check if the outpt is a valid JSON string.
// Check using the json.Unmarshal function.
// If the output is not a valid JSON string, the Unmarshal function will throw an error.
var result []map[string]interface{}
err := json.Unmarshal([]byte(render), &result)
if err != nil {
t.Error("Expected a valid JSON string, got", render)
}
}

8
export/types.go Normal file
View File

@ -0,0 +1,8 @@
package export
import "gitea.ligthert.net/golang/sudoku-funpark/controller"
// A simple inteface to export found solutions in different formats.
type Export struct {
Controller *controller.Controller
}

93
flags/ParseFlags.go Normal file
View File

@ -0,0 +1,93 @@
package flags
import (
"flag"
"fmt"
"os"
"runtime"
"strings"
)
// Parse command-line parameters, test input, store them in the Controller.
func (flags *Flags) ParseFlags() {
// Define parameters
flag.StringVar(&flags.Controller.Row1, "row1", "000000000", "1st row of the sudoku puzzle.")
flag.StringVar(&flags.Controller.Row2, "row2", "000000000", "2nd row of the sudoku puzzle.")
flag.StringVar(&flags.Controller.Row3, "row3", "000000000", "4rd row of the sudoku puzzle.")
flag.StringVar(&flags.Controller.Row4, "row4", "000000000", "4th row of the sudoku puzzle.")
flag.StringVar(&flags.Controller.Row5, "row5", "000000000", "5th row of the sudoku puzzle.")
flag.StringVar(&flags.Controller.Row6, "row6", "000000000", "6th row of the sudoku puzzle.")
flag.StringVar(&flags.Controller.Row7, "row7", "000000000", "7th row of the sudoku puzzle.")
flag.StringVar(&flags.Controller.Row8, "row8", "000000000", "8th row of the sudoku puzzle.")
flag.StringVar(&flags.Controller.Row9, "row9", "000000000", "9th row of the sudoku puzzle.")
flag.IntVar(&flags.Controller.NumCPUs, "numcpu", runtime.NumCPU(), "Number of CPU cores to assign to this task.")
flag.IntVar(&flags.Controller.Split, "split", 1, "Split the tasks in n parts. This depends on the availability of the first row.")
flag.IntVar(&flags.Controller.Part, "part", 1, "Process part x in n parts. Cannot be lower than 1, or higher than specified in split.")
flag.StringVar(&flags.Controller.Output, "output", "human", "Type of output. 'human' for human readable. 'flat' for flat as stored in memory output. 'json' for JSON output.")
flag.StringVar(&flags.Controller.Print, "print", "short", "'short': normal output;'long': normal output with timestamps; 'silent': Only print the results;")
// Parse the flags
flag.Parse()
// Process any changes to the CPU usage.
if flags.Controller.NumCPUs <= 0 {
fmt.Printf("ERROR: Number of CPU cores must be 1 or higher.\n\n")
flags.printUsage()
os.Exit(1)
}
if flags.Controller.NumCPUs != runtime.NumCPU() {
runtime.GOMAXPROCS(flags.Controller.NumCPUs)
}
// Process rows
if flags.Controller.Row1 == "000000000" || flags.Controller.Row2 == "000000000" || flags.Controller.Row3 == "000000000" || flags.Controller.Row4 == "000000000" || flags.Controller.Row5 == "000000000" || flags.Controller.Row6 == "000000000" || flags.Controller.Row7 == "000000000" || flags.Controller.Row8 == "000000000" || flags.Controller.Row9 == "000000000" {
fmt.Printf("ERROR: All parameters must be entered.\n\n")
flags.printUsage()
os.Exit(1)
}
// Validate the row (never trust user input)
flags.validateRow("row1", flags.Controller.Row1)
flags.validateRow("row2", flags.Controller.Row2)
flags.validateRow("row3", flags.Controller.Row3)
flags.validateRow("row4", flags.Controller.Row4)
flags.validateRow("row5", flags.Controller.Row5)
flags.validateRow("row6", flags.Controller.Row6)
flags.validateRow("row7", flags.Controller.Row7)
flags.validateRow("row8", flags.Controller.Row8)
flags.validateRow("row9", flags.Controller.Row9)
// Process workload splitting
// Ensure split and part are 1 or higher
if flags.Controller.Split <= 0 || flags.Controller.Part <= 0 {
fmt.Printf("ERROR: '-split' and '-part' need to be 1 or higher.\n")
flags.printUsage()
os.Exit(1)
}
// Ensure part is between 1 and split
if flags.Controller.Part > flags.Controller.Split {
fmt.Printf("ERROR: '-part' cannot be bigger than `-split`.\n")
flags.printUsage()
os.Exit(1)
}
// Process output selection
flags.Controller.Output = strings.ToLower(flags.Controller.Output)
if flags.Controller.Output != "human" && flags.Controller.Output != "flat" && flags.Controller.Output != "json" {
fmt.Printf("ERROR: Invalid output, can only be 'human' or 'flat' or 'json'.\n")
flags.printUsage()
os.Exit(1)
}
// Process print selection
flags.Controller.Print = strings.ToLower(flags.Controller.Print)
if flags.Controller.Print != "short" && flags.Controller.Print != "long" && flags.Controller.Print != "silent" {
fmt.Printf("ERROR: Invalid Print, can only be 'short' or 'long' or 'silent'.\n")
flags.printUsage()
os.Exit(1)
}
}

15
flags/printUsage.go Normal file
View File

@ -0,0 +1,15 @@
package flags
import (
"flag"
"fmt"
"os"
)
// Print help information for the end-user
func (flags *Flags) printUsage() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
fmt.Fprintf(flag.CommandLine.Output(), "\nPut every row of a Sudoku puzzle as paramters.\nUse '0' for what is currently blank in the puzzle you wish to solve.\n\n")
fmt.Fprintf(flag.CommandLine.Output(), "Example: %s -row1 ... -row2 ... -row3 ... (etc)\n\n", os.Args[0])
flag.PrintDefaults()
}

8
flags/types.go Normal file
View File

@ -0,0 +1,8 @@
package flags
import "gitea.ligthert.net/golang/sudoku-funpark/controller"
// Interface to parse command-line parameters
type Flags struct {
Controller *controller.Controller
}

14
flags/validChar.go Normal file
View File

@ -0,0 +1,14 @@
package flags
// Validate if the char provided is 0-9
func (flags *Flags) validChar(char rune) (valid bool) {
decvals := [10]int{48, 49, 50, 51, 52, 53, 54, 55, 56, 57}
for _, value := range decvals {
if char == rune(value) {
valid = true
}
}
return
}

39
flags/validChar_test.go Normal file
View File

@ -0,0 +1,39 @@
package flags
import "testing"
func TestValidChar(t *testing.T) {
tests := []struct {
name string
char rune
valid bool
}{
{
name: "Valid char",
char: '1',
valid: true,
},
{
name: "Invalid char",
char: 'a',
valid: false,
},
{
name: "Valid char but not relevant",
char: '0',
valid: true,
},
}
flags := Flags{}
// Run tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := flags.validChar(tt.char); got != tt.valid {
t.Errorf("validChar() = %v, want %v", got, tt.valid)
}
})
}
}

54
flags/validateRow.go Normal file
View File

@ -0,0 +1,54 @@
package flags
import (
"fmt"
"os"
)
// Validate if a row is properly set.
// This check for:
// - Correct length
// - Correct numbers
// - Numbers only present once
func (flags *Flags) validateRow(name string, row string) {
var found bool
var double bool
count := make(map[rune]int)
// 1. Make sure the row is 9 in length
if len(row) != 9 {
fmt.Printf("ERROR: Invalid length of %s (%s), must be 9 numbers\n\n", name, row)
flags.printUsage()
os.Exit(1)
}
// 2. Ensure all digits are numbers
for _, value := range row {
found = flags.validChar(value)
}
if !found {
fmt.Printf("ERROR: Invalid character of %s (%s), must be 9 numbers\n\n", name, row)
flags.printUsage()
os.Exit(1)
}
// 3. Ensure all digits (except zero) are there only once
for _, digits := range row {
count[digits] = count[digits] + 1
}
for key, value := range count {
if value > 1 && key != 48 {
double = true
}
}
if double {
fmt.Printf("ERROR: Double character of %s (%s), numbers between 1 and 9 may only be entered once\n\n", name, row)
flags.printUsage()
os.Exit(1)
}
}

49
main.go
View File

@ -1,10 +1,55 @@
package main
import (
"fmt"
"runtime"
"strconv"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/export"
"gitea.ligthert.net/golang/sudoku-funpark/flags"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
"gitea.ligthert.net/golang/sudoku-funpark/solver"
)
func main() {
// Run the meat of the program
solver.Run()
// Instantiate the interfaces
controller := controller.Controller{}
outp := outputter.Outputter{}
export := export.Export{Controller: &controller}
flags := flags.Flags{Controller: &controller}
solver := solver.Solver{Controller: &controller, Outp: &outp}
// Parse and handle flags
flags.ParseFlags()
// Tell outp what kind of output is expected.
outp.OutputType = controller.Print
// Report number of CPUs being used, if set.
if runtime.NumCPU() != controller.NumCPUs {
outp.Println("Using " + strconv.Itoa(controller.NumCPUs) + " CPUs, (was " + strconv.Itoa(runtime.NumCPU()) + ")")
}
// Load blocks from CSV file
solver.LoadBlocks()
// Find rows that fit with the entered rows
solver.PopulateBlocks()
// If needed, split the workload
// May exit and throw an error if the work load isn't viable
if controller.Split != 1 {
solver.SelectWorkload()
}
// Print the total number of solutions to validate
outp.Println("Number of (potential) solutions:", solver.Iter)
// Check the number of solutions
go solver.CheckCombinations()
solver.Tracker()
fmt.Println(export.Export())
}

View File

@ -1,80 +0,0 @@
Compat Matrix
1: 1 2 3 4 7
2: 1 2 3 5 8
3: 1 2 3 6 9
4: 1 4 5 6 7
5: 2 4 5 6 8
6: 3 4 5 6 9
7: 1 4 7 8 9
8: 2 5 7 8 9
9: 3 6 7 8 9
// counter: 123456789
// 1st Digit: 1 (49)
// 2nd Digit: 2 (50)
// 3rd Digit: 3 (51)
// 4th Digit: 4 (52)
// 5th Digit: 5 (53)
// 6th Digit: 6 (54)
// 7th Digit: 7 (55)
// 8th Digit: 8 (56)
// 9th Digit: 9 (57)
// 362880
// blocks := generate_blocks()
// fmt.Println(len(blocks))
// print_block(blocks[0])
// print_block(blocks[1])
// for i := range blocks {
// fmt.Println(blocks[i])
// }
row1 := "769104802"
row2 := "154800060"
row3 := "002700150"
row4 := "600900308"
row5 := "045328670"
row6 := "328670945"
row7 := "597410280"
row8 := "006283090"
row9 := "200590006"
row1 := "769154832"
row2 := "154832769"
row3 := "832769154"
row4 := "671945328"
row5 := "945328671"
row6 := "328671945"
row7 := "597416283"
row8 := "416284597"
row9 := "283597416"
// for rows1_index := range solver.row1s {
// for rows2_index := range solver.row2s {
// for rows3_index := range solver.row3s {
// for rows4_index := range solver.row4s {
// for rows5_index := range solver.row5s {
// for rows6_index := range solver.row6s {
// for rows7_index := range solver.row7s {
// for rows8_index := range solver.row8s {
// for rows9_index := range solver.row9s {
// go solver.routine_validator(rows1_index, rows2_index, rows3_index, rows4_index, rows5_index, rows6_index, rows7_index, rows8_index, rows9_index)
// // if solver.validate_combination(solver.row1s[rows1_index], solver.row2s[rows2_index], solver.row3s[rows3_index], solver.row4s[rows4_index], solver.row5s[rows5_index], solver.row6s[rows6_index], solver.row7s[rows7_index], solver.row8s[rows8_index], solver.row9s[rows9_index]) {
// // solver.solutions = append(solver.solutions, solver.render_combination(solver.row1s[rows1_index], solver.row2s[rows2_index], solver.row3s[rows3_index], solver.row4s[rows4_index], solver.row5s[rows5_index], solver.row6s[rows6_index], solver.row7s[rows7_index], solver.row8s[rows8_index], solver.row9s[rows9_index]))
// // }
// // solver.counter = solver.counter + 1
// // if solver.counter%1000000 == 0 {
// // percentage = (float32(solver.counter) / (float32(solver.iter) / 100))
// // fmt.Println("Processing:", percentage, "%; Procs:", runtime.NumGoroutine())
// // }
// }
// }
// }
// }
// }
// }
// }
// }
// }

17
outputter/Printf.go Normal file
View File

@ -0,0 +1,17 @@
package outputter
import (
"fmt"
"log"
)
func (outputter *Outputter) Printf(format string, args ...any) {
switch outputter.OutputType {
case "short":
fmt.Printf(format, args...)
case "long":
log.Printf(format, args...)
case "silent":
// Do nothing
}
}

17
outputter/Println.go Normal file
View File

@ -0,0 +1,17 @@
package outputter
import (
"fmt"
"log"
)
func (outputter *Outputter) Println(msg ...any) {
switch outputter.OutputType {
case "short":
fmt.Println(msg...)
case "long":
log.Println(msg...)
case "silent":
// Do nothing
}
}

5
outputter/types.go Normal file
View File

@ -0,0 +1,5 @@
package outputter
type Outputter struct {
OutputType string
}

View File

@ -0,0 +1,24 @@
package solver
// Iterate through all combination of blocks and validate them.
func (solver *Solver) CheckCombinations() {
for rows1Index := range solver.row1s {
for rows2Index := range solver.row2s {
for rows3Index := range solver.row3s {
for rows4Index := range solver.row4s {
for rows5Index := range solver.row5s {
for rows6Index := range solver.row6s {
for rows7Index := range solver.row7s {
for rows8Index := range solver.row8s {
for rows9Index := range solver.row9s {
go solver.validator(rows1Index, rows2Index, rows3Index, rows4Index, rows5Index, rows6Index, rows7Index, rows8Index, rows9Index)
}
}
}
}
}
}
}
}
}
}

View File

@ -2,8 +2,6 @@ package solver
import (
"embed"
"log"
"strconv"
"strings"
"time"
)
@ -13,12 +11,13 @@ import (
//go:embed blocks.csv
var f embed.FS
func (solver *Solver) load_blocks() {
// Load all possible blocks from CSV in to memory
func (solver *Solver) LoadBlocks() {
defer solver.timeTrack(time.Now(), "Loaded blocks")
log.Println("Loading blocks")
defer solver.timeTrack(time.Now(), "Done!")
solver.Outp.Printf("Loading blocks... ")
var blocks []int
var blocks []string
file, err := f.ReadFile("blocks.csv")
if err != nil {
@ -28,13 +27,13 @@ func (solver *Solver) load_blocks() {
temp := strings.Split(string(file), "\n")
for _, line := range temp {
block, _ := strconv.Atoi(string(line))
if block == 0 { // Ignore new-line at the end of the file.
block := string(line)
if block == "\n" || block == "" { // Ignore new-line at the end of the file.
continue
}
blocks = append(blocks, block)
}
solver.blocks = blocks
solver.Controller.Blocks = blocks
}

40
solver/LoadBlocks_test.go Normal file
View File

@ -0,0 +1,40 @@
package solver
import (
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// This function will test the loadBlocks function.
// Starts off by creating a new Solver struct.
// Then it will execute the loadBlocks function.
// Check if there are 9! blocks loaded.
// Check if the first block is "123456789".
// check if the last block is "987654321".
func TestLoadBlocks(t *testing.T) {
// Do all the necesarry steps to initialize the solver.
solver := Solver{}
outp := outputter.Outputter{}
outp.OutputType = "short"
solver.Outp = &outp
controller := controller.Controller{}
solver.Controller = &controller
solver.LoadBlocks()
// Check if there are 9! blocks loaded.
if len(solver.Controller.Blocks) != 362880 {
t.Error("Expected 362880, got", len(solver.Controller.Blocks))
}
// Check if the first block is "123456789".
if solver.Controller.Blocks[0] != "123456789" {
t.Error("Expected 123456789, got", solver.Controller.Blocks[0])
}
// Check if the last block is "987654321".
if solver.Controller.Blocks[362879] != "987654321" {
t.Error("Expected 987654321, got", solver.Controller.Blocks[362879])
}
}

26
solver/PopulateBlocks.go Normal file
View File

@ -0,0 +1,26 @@
package solver
import (
"time"
)
// Find all possible blocks that can be used to find a solution.
func (solver *Solver) PopulateBlocks() {
defer solver.timeTrack(time.Now(), "Done!")
solver.Outp.Printf("Populating blocks... ")
solver.findBlocks(&solver.Controller.Row1, &solver.row1s)
solver.findBlocks(&solver.Controller.Row2, &solver.row2s)
solver.findBlocks(&solver.Controller.Row3, &solver.row3s)
solver.findBlocks(&solver.Controller.Row4, &solver.row4s)
solver.findBlocks(&solver.Controller.Row5, &solver.row5s)
solver.findBlocks(&solver.Controller.Row6, &solver.row6s)
solver.findBlocks(&solver.Controller.Row7, &solver.row7s)
solver.findBlocks(&solver.Controller.Row8, &solver.row8s)
solver.findBlocks(&solver.Controller.Row9, &solver.row9s)
// This calculates and stores the total number of solutions to validate.
solver.Iter = uint64(len(solver.row1s)) * uint64(len(solver.row2s)) * uint64(len(solver.row3s)) * uint64(len(solver.row4s)) * uint64(len(solver.row5s)) * uint64(len(solver.row6s)) * uint64(len(solver.row7s)) * uint64(len(solver.row8s)) * uint64(len(solver.row9s))
}

View File

@ -0,0 +1,90 @@
package solver
import (
"slices"
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// This functions tests the PopulateBlocks function.
// It does this by creating a small set of rows and then
// calling the PopulateBlocks function. It then checks
// if the number of blocks is as expected.
func TestPopulateBlocks(t *testing.T) {
// Do all the necesarry steps to initialize the solver.
solver := Solver{}
outp := outputter.Outputter{}
outp.OutputType = "short"
solver.Outp = &outp
controller := controller.Controller{}
solver.Controller = &controller
solver.LoadBlocks()
// Create a set of rows
// Using the following values: [769154832 154832769 832769154 671945328 945328671 328671945 597416283 416283597 283597416]
solver.Controller.Row1 = "769154800"
solver.Controller.Row2 = "154832700"
solver.Controller.Row3 = "832769100"
solver.Controller.Row4 = "671945300"
solver.Controller.Row5 = "945328600"
solver.Controller.Row6 = "328671900"
solver.Controller.Row7 = "597416200"
solver.Controller.Row8 = "416283500"
solver.Controller.Row9 = "283597400"
// Call the PopulateBlocks function
solver.PopulateBlocks()
// Check if the number of blocks is as expected
if solver.Iter != 512 {
t.Errorf("Expected 19683 blocks, got %d", solver.Iter)
}
// Check if solver.row1s is as expected
if slices.Compare(solver.row1s, []string{"769154823", "769154832"}) != 0 {
t.Errorf("Expected [769154832, 769154823], got %s", solver.row1s)
}
// check if solver.row2s is as expected
if slices.Compare(solver.row2s, []string{"154832769", "154832796"}) != 0 {
t.Errorf("Expected [154832769, 154832796], got %s", solver.row2s)
}
// check if solver.row3s is as expected
if slices.Compare(solver.row3s, []string{"832769145", "832769154"}) != 0 {
t.Errorf("Expected [832769154, 832769145], got %s", solver.row3s)
}
// check if solver.row4s is as expected
if slices.Compare(solver.row4s, []string{"671945328", "671945382"}) != 0 {
t.Errorf("Expected [671945328, 671945382], got %s", solver.row4s)
}
// check if solver.row5s is as expected
if slices.Compare(solver.row5s, []string{"945328617", "945328671"}) != 0 {
t.Errorf("Expected [945328617, 945328671], got %s", solver.row5s)
}
// check if solver.row6s is as expected
if slices.Compare(solver.row6s, []string{"328671945", "328671954"}) != 0 {
t.Errorf("Expected [328671945, 328671954], got %s", solver.row6s)
}
// check if solver.row7s is as expected
if slices.Compare(solver.row7s, []string{"597416238", "597416283"}) != 0 {
t.Errorf("Expected [597416238, 597416283], got %s", solver.row7s)
}
// check if solver.row8s is as expected
if slices.Compare(solver.row8s, []string{"416283579", "416283597"}) != 0 {
t.Errorf("Expected [416283579, 416283597], got %s", solver.row8s)
}
// check if solver.row9s is as expected
if slices.Compare(solver.row9s, []string{"283597416", "283597461"}) != 0 {
t.Errorf("Expected [283597416, 283597461], got %s", solver.row9s)
}
}

22
solver/SelectWorkload.go Normal file
View File

@ -0,0 +1,22 @@
package solver
import (
"os"
"strconv"
"time"
)
// Renders workload for an agent.
// Checks if this feature can be used, otherwise exits.
// Modify solver.row1s so it limits the workload to what is only desired
func (solver *Solver) SelectWorkload() {
if solver.Controller.Split > len(solver.row1s) {
solver.Outp.Println("ERROR: Unable to divide the workload in " + strconv.Itoa(solver.Controller.Split) + " parts, when only " + strconv.Itoa(len(solver.row1s)) + " are available.\n\n")
os.Exit(1)
}
defer solver.timeTrack(time.Now(), "Workload set")
solver.Outp.Println("Setting workload")
solver.Outp.Println("We are agent " + strconv.Itoa(solver.Controller.Part) + " of " + strconv.Itoa(solver.Controller.Split))
workloads := solver.splitWorkload()
solver.setWorkload(workloads)
}

102
solver/Tracker.go Normal file
View File

@ -0,0 +1,102 @@
package solver
import (
"strconv"
"time"
)
// Keep track and output progress.
// Calculate rates, display percentages, estimate the ETA till completion.
func (solver *Solver) Tracker() {
// Add time tracking
defer solver.timeTrack(time.Now(), "Validated solutions")
solver.Outp.Println("Validating solutions")
// Determine if the main-loop is done
var done bool
// Tracking progress in percentages
var percentage float32
// Tracking progress in validated solutions
var track int
// Tracking the rate, starting point
var rateStart uint64
// Tracking the rate, difference between previous iterations
var rateDiff uint64
// Tracking duration
var timerStart = time.Now()
// Estimation how long it will take
var est_fin string
// While not needed for rateDiff anymore, it makes estimation calculations more accurate. ☹️
time.Sleep(time.Second)
// for solver.Iter != solver.counter { // Start for-loop
for !done {
// Determine how far we are.
percentage = (float32(solver.counter.Load()) / (float32(solver.Iter) / 100))
// Reset the loop
rateDiff = solver.counter.Load() - rateStart
if track <= int(percentage) || rateDiff == 0 { // Start if-statement
// Make sure something happened, making rateStart the only reliable variable
if solver.Iter == solver.counter.Load() {
percentage = 100
solver.counter.Store(solver.Iter)
done = true
}
timer_elapsed := time.Since(timerStart)
solver.rates = append(solver.rates, rateDiff)
rate_avg := solver.calcAVG()
// Estimate when this is finished
if rateDiff == 0 {
est_fin = "N/A"
} else {
duration_int := (solver.Iter - solver.counter.Load()) / rate_avg
duration_string := strconv.Itoa(int(duration_int)) + "s"
est, err := time.ParseDuration(duration_string)
if err != nil {
est_fin = "parse error"
} else {
est_fin = est.String()
}
}
// Printing the progress
solver.Outp.Println("Processing: " + strconv.Itoa(int(percentage)) + "% (" + strconv.FormatUint(solver.counter.Load(), 10) + "/" + strconv.Itoa(int(solver.Iter)) + "); Rate: " + strconv.FormatUint(rateDiff, 10) + "/sec for " + timer_elapsed.String() + "; Time left (est.): " + est_fin)
// After we are done printing, exit this for-loop
if percentage == 100 {
break
}
// Wrap up the loop or break
if int(percentage) > track {
track = int(percentage)
} else {
track = track + 1
}
timerStart = time.Now()
}
// Resert the rate counter
rateStart = solver.counter.Load()
// Sleep for a second
if solver.Iter != solver.counter.Load() {
time.Sleep(1 * time.Second)
}
} // End for-loop
}

14
solver/calcAVG.go Normal file
View File

@ -0,0 +1,14 @@
package solver
// Calculate the average rate in a stored slice of rates.
func (solver *Solver) calcAVG() (avg uint64) {
var avgSum uint64
for _, value := range solver.rates {
avgSum += uint64(value)
}
avg = avgSum / uint64(len(solver.rates))
return
}

33
solver/calcAVG_test.go Normal file
View File

@ -0,0 +1,33 @@
package solver
import (
"testing"
)
// Test the calcAVG function.
func TestCalcAVG(t *testing.T) {
// Create a new Solver struct.
solver := Solver{}
// Populate the solver.rates[] slice with some values and check if the average is calculated correctly.
solver.rates = []uint64{1, 2, 3, 4, 5}
if solver.calcAVG() != 3 {
t.Error("Expected 3, got", solver.calcAVG())
}
// Populate the solver.Rate[] slice with some values and check if the average is calculated correctly.
solver.rates = []uint64{1, 2, 3, 4, 5, 6}
if solver.calcAVG() != 3 {
t.Error("Expected 3, got", solver.calcAVG())
}
// Populate the solver.Rate[] slice with some values and check if the average is calculated correctly.
solver.rates = []uint64{1, 2, 3, 4, 5, 6, 7}
if solver.calcAVG() != 4 {
t.Error("Expected 4, got", solver.calcAVG())
}
// Populate the solver.Rate[] slice with some values and check if the average is calculated correctly.
solver.rates = []uint64{1, 2, 3, 4, 5, 6, 7, 8}
if solver.calcAVG() != 4 {
t.Error("Expected 4, got", solver.calcAVG())
}
}

37
solver/findBlocks.go Normal file
View File

@ -0,0 +1,37 @@
package solver
// The actual function that finds the blocks matching the partial blocks.
func (solver *Solver) findBlocks(row *string, rows *[]string) {
// Declare selection
var selection []string
var currBlocks []string
funcRow := *row
for letter := range funcRow {
if len(selection) == 0 {
currBlocks = solver.Controller.Blocks
} else {
currBlocks = selection
selection = nil
}
for _, block := range currBlocks {
currRow := block
if funcRow[letter] == currRow[letter] {
foundRow := currRow
selection = append(selection, foundRow)
}
if funcRow[letter] == '0' {
foundRow := currRow
selection = append(selection, foundRow)
}
} // End for-loop
} // End for-loop
*rows = selection
}

53
solver/findBlocks_test.go Normal file
View File

@ -0,0 +1,53 @@
package solver
import (
"slices"
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// TestfindBlocks tests the findBlocks function.
// It defines the solver struct
// and calls the findBlocks function with a row and a slice of rows.
// It then checks if the slice of rows is as expected.
func TestFindBlocks(t *testing.T) {
// Create a new Solver struct.
solver := Solver{}
// Create outputter struct.
outp := outputter.Outputter{}
// Set the output type to human.
outp.OutputType = "human"
// Set the outputter of the solver to the outputter.
solver.Outp = &outp
// Create a controller struct.
controller := controller.Controller{}
// Set the controller of the solver to the controller.
solver.Controller = &controller
// Execute the loadBlocks function.
solver.LoadBlocks()
// Provide controller.row1 with a value.
// This is the row that will be used in the findBlocks function.
controller.Row1 = "769104802"
// Call the findBlocks function with the row and the slice of rows.
solver.findBlocks(&controller.Row1, &solver.row1s)
// A slice with the expected results.
expected := []string{"769154832", "769134852"}
// Check if the slice of rows1 is same as expected variable.
if slices.Equal(solver.row1s, expected) {
t.Errorf("Expected %v, got %v", expected, solver.row1s)
}
}

View File

@ -1,128 +0,0 @@
package solver
import (
"flag"
"fmt"
"log"
"os"
)
func (solver *Solver) parse_flags() {
// Define variables
var row1 string
var row2 string
var row3 string
var row4 string
var row5 string
var row6 string
var row7 string
var row8 string
var row9 string
// Define parameters
flag.StringVar(&row1, "row1", "000000000", "1st row of the sudoku puzzle.")
flag.StringVar(&row2, "row2", "000000000", "2nd row of the sudoku puzzle.")
flag.StringVar(&row3, "row3", "000000000", "4rd row of the sudoku puzzle.")
flag.StringVar(&row4, "row4", "000000000", "4th row of the sudoku puzzle.")
flag.StringVar(&row5, "row5", "000000000", "5th row of the sudoku puzzle.")
flag.StringVar(&row6, "row6", "000000000", "6th row of the sudoku puzzle.")
flag.StringVar(&row7, "row7", "000000000", "7th row of the sudoku puzzle.")
flag.StringVar(&row8, "row8", "000000000", "8th row of the sudoku puzzle.")
flag.StringVar(&row9, "row9", "000000000", "9th row of the sudoku puzzle.")
// Parse the flags
flag.Parse()
if row1 == "000000000" || row2 == "000000000" || row3 == "000000000" || row4 == "000000000" || row5 == "000000000" || row6 == "000000000" || row7 == "000000000" || row8 == "000000000" || row9 == "000000000" {
log.Printf("ERROR: All parameters must be entered.\n\n")
solver.print_Usage()
os.Exit(1)
}
// Validate the row (never trust user input)
solver.validate_row("row1", row1)
solver.validate_row("row2", row2)
solver.validate_row("row3", row3)
solver.validate_row("row4", row4)
solver.validate_row("row5", row5)
solver.validate_row("row6", row6)
solver.validate_row("row7", row7)
solver.validate_row("row8", row8)
solver.validate_row("row9", row9)
// Put entries in into the struct
solver.row1 = row1
solver.row2 = row2
solver.row3 = row3
solver.row4 = row4
solver.row5 = row5
solver.row6 = row6
solver.row7 = row7
solver.row8 = row8
solver.row9 = row9
}
func (solver *Solver) validate_row(name string, row string) {
var found bool
var double bool
count := make(map[rune]int)
// 1. Make sure the row is 9 in length
if len(row) != 9 {
log.Printf("ERROR: Invalid length of %s (%s), must be 9 numbers\n\n", name, row)
solver.print_Usage()
os.Exit(1)
}
// 2. Ensure all digits are numbers
for _, value := range row {
found = solver.valid_char(value)
}
if !found {
log.Printf("ERROR: Invalid character of %s (%s), must be 9 numbers\n\n", name, row)
solver.print_Usage()
os.Exit(1)
}
// 3. Ensure all digits (except zero) are there only once
for _, digits := range row {
count[digits] = count[digits] + 1
}
for key, value := range count {
if value > 1 && key != 48 {
double = true
}
}
if double {
log.Printf("ERROR: Double character of %s (%s), numbers between 1 and 9 may only be entered once\n\n", name, row)
solver.print_Usage()
os.Exit(1)
}
}
func (solver *Solver) valid_char(char rune) bool {
var valid bool
decvals := [10]int{48, 49, 50, 51, 52, 53, 54, 55, 56, 57}
for _, value := range decvals {
if char == rune(value) {
valid = true
}
}
return valid
}
func (solver *Solver) print_Usage() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
fmt.Fprintf(flag.CommandLine.Output(), "\nPut every row of a Sudoku puzzle as paramters.\nUse '0' for what is currently blank in the puzzle you wish to solve.\n\n")
fmt.Fprintf(flag.CommandLine.Output(), "Example: %s -row1 ... -row2 ... -row3 ... (etc)\n\n", os.Args[0])
flag.PrintDefaults()
}

View File

@ -1,13 +0,0 @@
package solver
import (
"fmt"
"log"
)
func (solver *Solver) print_solutions() {
for solution_index, solution := range solver.solutions {
log.Printf("\nSolution #%d:", solution_index+1)
fmt.Println(solution)
}
}

View File

@ -1,221 +0,0 @@
package solver
import (
"log"
"strconv"
"time"
)
func (solver *Solver) populate_blocks() {
defer solver.timeTrack(time.Now(), "Populated blocks")
log.Println("Populating blocks")
solver.find_blocks(&solver.row1, &solver.row1s)
solver.find_blocks(&solver.row2, &solver.row2s)
solver.find_blocks(&solver.row3, &solver.row3s)
solver.find_blocks(&solver.row4, &solver.row4s)
solver.find_blocks(&solver.row5, &solver.row5s)
solver.find_blocks(&solver.row6, &solver.row6s)
solver.find_blocks(&solver.row7, &solver.row7s)
solver.find_blocks(&solver.row8, &solver.row8s)
solver.find_blocks(&solver.row9, &solver.row9s)
// This calculates and stores the total number of solutions to validate.
solver.iter = int64(len(solver.row1s)) * int64(len(solver.row2s)) * int64(len(solver.row3s)) * int64(len(solver.row4s)) * int64(len(solver.row5s)) * int64(len(solver.row6s)) * int64(len(solver.row7s)) * int64(len(solver.row8s)) * int64(len(solver.row9s))
}
func (solver *Solver) find_blocks(row *string, rows *[]int) {
// Declare selection
var selection []int
var curr_blocks []int
func_row := *row
for letter := range func_row {
if len(selection) == 0 {
curr_blocks = solver.blocks
} else {
curr_blocks = selection
selection = nil
}
for _, block := range curr_blocks {
curr_row := strconv.Itoa(block)
if func_row[letter] == curr_row[letter] {
found_row, _ := strconv.Atoi(curr_row)
selection = append(selection, found_row)
}
if func_row[letter] == '0' {
found_row, _ := strconv.Atoi(curr_row)
selection = append(selection, found_row)
}
} // End for-loop
} // End for-loop
*rows = selection
}
func (solver *Solver) check_combinations() {
for rows1_index := range solver.row1s {
for rows2_index := range solver.row2s {
for rows3_index := range solver.row3s {
for rows4_index := range solver.row4s {
for rows5_index := range solver.row5s {
for rows6_index := range solver.row6s {
for rows7_index := range solver.row7s {
for rows8_index := range solver.row8s {
for rows9_index := range solver.row9s {
go solver.routine_validator(rows1_index, rows2_index, rows3_index, rows4_index, rows5_index, rows6_index, rows7_index, rows8_index, rows9_index)
}
}
}
}
}
}
}
}
}
}
func (solver *Solver) routine_validator(rows1_index int, rows2_index int, rows3_index int, rows4_index int, rows5_index int, rows6_index int, rows7_index int, rows8_index int, rows9_index int) {
solver.counter = solver.counter + 1
if solver.validate_combination(solver.row1s[rows1_index], solver.row2s[rows2_index], solver.row3s[rows3_index], solver.row4s[rows4_index], solver.row5s[rows5_index], solver.row6s[rows6_index], solver.row7s[rows7_index], solver.row8s[rows8_index], solver.row9s[rows9_index]) {
solver.solutions = append(solver.solutions, solver.render_combination(solver.row1s[rows1_index], solver.row2s[rows2_index], solver.row3s[rows3_index], solver.row4s[rows4_index], solver.row5s[rows5_index], solver.row6s[rows6_index], solver.row7s[rows7_index], solver.row8s[rows8_index], solver.row9s[rows9_index]))
}
}
func (solver *Solver) tracker() {
defer solver.timeTrack(time.Now(), "Validated solutions")
log.Println("Validating solutions")
// Tracking percenting an progress
var percentage float32
var track int
// Tracking the rate
var rate_start int
var rate_stop int
var rate_diff int
// Tracking duration
var timer_start = time.Now()
// Prevent division-by-zero error when establishing `rate`
time.Sleep(time.Second)
// Estimation how long it will take
var est_fin string
for solver.iter != solver.counter {
// Determine how far we are.
percentage = (float32(solver.counter) / (float32(solver.iter) / 100))
if track <= int(percentage) {
// Reset the loop
rate_stop = int(solver.counter)
rate_diff = rate_stop - rate_start
// Make sure something happened, making rate_start the only reliable variable
if rate_diff == 0 && rate_start > 1 {
percentage = 100
solver.counter = solver.iter
}
timer_elapsed := time.Since(timer_start)
rate := int64(rate_diff) / int64(timer_elapsed.Seconds())
// Estimate when this is finished:
// TODO: Make this Bayesian
if rate_diff == 0 {
est_fin = "N/A"
} else {
est_fin = solver.secondsToHuman((int(solver.iter) - int(solver.counter)) / int(rate))
}
// Printing the meat
log.Println("Processing: " + strconv.Itoa(int(percentage)) + "% (" + strconv.Itoa(int(solver.counter)) + "/" + strconv.Itoa(int(solver.iter)) + "); Rate (avg): " + strconv.Itoa(int(rate)) + "/sec for " + timer_elapsed.String() + "; Time left (est.): " + est_fin)
// Wrap up the loop or break
if int(percentage) > track {
track = int(percentage)
} else {
track = track + 1
}
rate_start = rate_stop
timer_start = time.Now()
if rate_diff == 0 && rate_start > 100 {
break
}
}
time.Sleep(1 * time.Second)
}
}
func (solver *Solver) validate_combination(row1 int, row2 int, row3 int, row4 int, row5 int, row6 int, row7 int, row8 int, row9 int) bool {
var retval bool
retval = true
row1s := strconv.Itoa(row1)
row2s := strconv.Itoa(row2)
row3s := strconv.Itoa(row3)
row4s := strconv.Itoa(row4)
row5s := strconv.Itoa(row5)
row6s := strconv.Itoa(row6)
row7s := strconv.Itoa(row7)
row8s := strconv.Itoa(row8)
row9s := strconv.Itoa(row9)
for index := range 9 {
if row1s[index] == row2s[index] || row1s[index] == row3s[index] || row1s[index] == row4s[index] || row1s[index] == row5s[index] || row1s[index] == row6s[index] || row1s[index] == row7s[index] || row1s[index] == row8s[index] || row1s[index] == row9s[index] {
retval = false
}
if row2s[index] == row1s[index] || row2s[index] == row3s[index] || row2s[index] == row4s[index] || row2s[index] == row5s[index] || row2s[index] == row6s[index] || row2s[index] == row7s[index] || row2s[index] == row8s[index] || row2s[index] == row9s[index] {
retval = false
}
if row3s[index] == row1s[index] || row3s[index] == row2s[index] || row3s[index] == row4s[index] || row3s[index] == row5s[index] || row3s[index] == row6s[index] || row3s[index] == row7s[index] || row3s[index] == row8s[index] || row3s[index] == row9s[index] {
retval = false
}
if row4s[index] == row1s[index] || row4s[index] == row2s[index] || row4s[index] == row3s[index] || row4s[index] == row5s[index] || row4s[index] == row6s[index] || row4s[index] == row7s[index] || row4s[index] == row8s[index] || row4s[index] == row9s[index] {
retval = false
}
if row5s[index] == row1s[index] || row5s[index] == row2s[index] || row5s[index] == row3s[index] || row5s[index] == row4s[index] || row5s[index] == row6s[index] || row5s[index] == row7s[index] || row5s[index] == row8s[index] || row5s[index] == row9s[index] {
retval = false
}
if row6s[index] == row1s[index] || row6s[index] == row2s[index] || row6s[index] == row3s[index] || row6s[index] == row4s[index] || row6s[index] == row5s[index] || row6s[index] == row7s[index] || row6s[index] == row8s[index] || row6s[index] == row9s[index] {
retval = false
}
if row7s[index] == row1s[index] || row7s[index] == row2s[index] || row7s[index] == row3s[index] || row7s[index] == row4s[index] || row5s[index] == row6s[index] || row7s[index] == row6s[index] || row7s[index] == row8s[index] || row7s[index] == row9s[index] {
retval = false
}
if row8s[index] == row1s[index] || row8s[index] == row2s[index] || row8s[index] == row3s[index] || row8s[index] == row4s[index] || row8s[index] == row5s[index] || row8s[index] == row6s[index] || row8s[index] == row7s[index] || row8s[index] == row9s[index] {
retval = false
}
if row9s[index] == row1s[index] || row9s[index] == row2s[index] || row9s[index] == row3s[index] || row9s[index] == row4s[index] || row9s[index] == row5s[index] || row9s[index] == row6s[index] || row9s[index] == row7s[index] || row9s[index] == row8s[index] {
retval = false
}
}
return retval
}

21
solver/setWorkload.go Normal file
View File

@ -0,0 +1,21 @@
package solver
// Set the workload by setting solver.row1s
func (solver *Solver) setWorkload(agents []int) {
var start int = 0
var finish int = 0
for key, value := range agents {
if key == solver.Controller.Part-1 {
finish = start + value
break
} else {
start += value
}
}
// Set the shortened set of instructions
solver.row1s = solver.row1s[start:finish]
// Recalculate how much we need to grind through
solver.Iter = uint64(len(solver.row1s)) * uint64(len(solver.row2s)) * uint64(len(solver.row3s)) * uint64(len(solver.row4s)) * uint64(len(solver.row5s)) * uint64(len(solver.row6s)) * uint64(len(solver.row7s)) * uint64(len(solver.row8s)) * uint64(len(solver.row9s))
}

View File

@ -0,0 +1,72 @@
package solver
import (
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// Function to test the setWorkload function.
func TestSetWorkload(t *testing.T) {
// Create a new Solver struct.
solver := Solver{}
// Create outputter struct.
outp := outputter.Outputter{}
// Set the output type to human.
outp.OutputType = "short"
// Set the outputter of the solver to the outputter.
solver.Outp = &outp
// Create a controller struct.
controller := controller.Controller{}
// Set the controller of the solver to the controller.
solver.Controller = &controller
// Execute the loadBlocks function.
solver.LoadBlocks()
// Provide controller.row1 with a value.
// This is the row that will be used in the findBlocks function.
controller.Row1 = "769104802"
// Fill the other rows with values.
solver.row2s = []string{"769104802"}
solver.row3s = []string{"769104802"}
solver.row4s = []string{"769104802"}
solver.row5s = []string{"769104802"}
solver.row6s = []string{"769104802"}
solver.row7s = []string{"769104802"}
solver.row8s = []string{"769104802"}
solver.row9s = []string{"769104802"}
// Call the findBlocks function with the row and the slice of rows.
solver.findBlocks(&controller.Row1, &solver.row1s)
// Divide the work between two agents.
solver.Controller.Split = 2
// Set the part of the workload that the first agent will do.
solver.Controller.Part = 1
// Call the splitWorkload function.
agents := solver.splitWorkload()
// Run the setWorkload function.
solver.setWorkload(agents)
// Check if the solver.row1s slice is as expected.
if len(solver.row1s) != 1 {
t.Errorf("Expected 1, got %v", len(solver.row1s))
}
// Check if the solver.Iter value is as expected.
if solver.Iter != 1 {
t.Errorf("Expected 9, got %v", solver.Iter)
}
}

View File

@ -1,30 +0,0 @@
package solver
import (
"log"
)
func Run() {
// Instantiate the Solver interface
solver := Solver{}
// Parse and handle flags
solver.parse_flags()
// Load blocks from CSV file
solver.load_blocks()
// Find rows that fit with the entered rows
solver.populate_blocks()
// Print the total number of solutions to validate
log.Println("Number of (potential) solutions:", solver.iter)
// Check the number of solutions
go solver.check_combinations()
solver.tracker()
// Print the valid solutions
solver.print_solutions()
}

19
solver/splitWorkload.go Normal file
View File

@ -0,0 +1,19 @@
package solver
// Determine how workload should be split among the agents
func (solver *Solver) splitWorkload() []int {
agents := make([]int, solver.Controller.Split)
var tracker int
var tasks int = len(solver.row1s)
for tasks != 0 {
agents[tracker] += 1
tasks -= 1
tracker += 1
if tracker == solver.Controller.Split {
tracker = 0
}
}
return agents
}

View File

@ -0,0 +1,51 @@
package solver
import (
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// Test the splitWorkload function.
func TestSplitWorkload(t *testing.T) {
// Create a new Solver struct.
solver := Solver{}
// Create outputter struct.
outp := outputter.Outputter{}
// Set the output type to human.
outp.OutputType = "human"
// Set the outputter of the solver to the outputter.
solver.Outp = &outp
// Create a controller struct.
controller := controller.Controller{}
// Set the controller of the solver to the controller.
solver.Controller = &controller
// Execute the loadBlocks function.
solver.LoadBlocks()
// Provide controller.row1 with a value.
// This is the row that will be used in the findBlocks function.
controller.Row1 = "769104802"
// Call the findBlocks function with the row and the slice of rows.
solver.findBlocks(&controller.Row1, &solver.row1s)
// Divide the work between two agents.
solver.Controller.Split = 2
// Call the splitWorkload function.
agents := solver.splitWorkload()
// Check if the agents slice is as expected.
if agents[0] != 1 || agents[1] != 1 {
t.Errorf("Expected [1, 1], got %v", agents)
}
}

View File

@ -1,20 +0,0 @@
package solver
import (
"strconv"
)
func (solver *Solver) render_combination(row1 int, row2 int, row3 int, row4 int, row5 int, row6 int, row7 int, row8 int, row9 int) string {
row1s := strconv.Itoa(row1)
row2s := strconv.Itoa(row2)
row3s := strconv.Itoa(row3)
row4s := strconv.Itoa(row4)
row5s := strconv.Itoa(row5)
row6s := strconv.Itoa(row6)
row7s := strconv.Itoa(row7)
row8s := strconv.Itoa(row8)
row9s := strconv.Itoa(row9)
return row1s + "\n" + row2s + "\n" + row3s + "\n" + row4s + "\n" + row5s + "\n" + row6s + "\n" + row7s + "\n" + row8s + "\n" + row9s + "\n"
}

12
solver/timeTrack.go Normal file
View File

@ -0,0 +1,12 @@
package solver
import (
"time"
)
// A simple function to track time
// Use with `defer`
func (solver *Solver) timeTrack(start time.Time, msg string) {
elapsed := time.Since(start)
solver.Outp.Printf("%s (%s)\n", msg, elapsed)
}

View File

@ -1,56 +0,0 @@
package solver
import (
"log"
"math"
"strconv"
"time"
)
func (solver *Solver) timeTrack(start time.Time, msg string) {
elapsed := time.Since(start)
log.Printf("%s (%s)", msg, elapsed)
}
// Stolen from https://socketloop.com/tutorials/golang-convert-seconds-to-human-readable-time-format-example
func (solver *Solver) plural(count int, singular string) (result string) {
if (count == 1) || (count == 0) {
result = strconv.Itoa(count) + " " + singular + " "
} else {
result = strconv.Itoa(count) + " " + singular + "s "
}
return
}
func (solver *Solver) secondsToHuman(input int) (result string) {
years := math.Floor(float64(input) / 60 / 60 / 24 / 7 / 30 / 12)
seconds := input % (60 * 60 * 24 * 7 * 30 * 12)
months := math.Floor(float64(seconds) / 60 / 60 / 24 / 7 / 30)
seconds = input % (60 * 60 * 24 * 7 * 30)
weeks := math.Floor(float64(seconds) / 60 / 60 / 24 / 7)
seconds = input % (60 * 60 * 24 * 7)
days := math.Floor(float64(seconds) / 60 / 60 / 24)
seconds = input % (60 * 60 * 24)
hours := math.Floor(float64(seconds) / 60 / 60)
seconds = input % (60 * 60)
minutes := math.Floor(float64(seconds) / 60)
seconds = input % 60
if years > 0 {
result = solver.plural(int(years), "year") + solver.plural(int(months), "month") + solver.plural(int(weeks), "week") + solver.plural(int(days), "day") + solver.plural(int(hours), "hour") + solver.plural(int(minutes), "minute") + solver.plural(int(seconds), "second")
} else if months > 0 {
result = solver.plural(int(months), "month") + solver.plural(int(weeks), "week") + solver.plural(int(days), "day") + solver.plural(int(hours), "hour") + solver.plural(int(minutes), "minute") + solver.plural(int(seconds), "second")
} else if weeks > 0 {
result = solver.plural(int(weeks), "week") + solver.plural(int(days), "day") + solver.plural(int(hours), "hour") + solver.plural(int(minutes), "minute") + solver.plural(int(seconds), "second")
} else if days > 0 {
result = solver.plural(int(days), "day") + solver.plural(int(hours), "hour") + solver.plural(int(minutes), "minute") + solver.plural(int(seconds), "second")
} else if hours > 0 {
result = solver.plural(int(hours), "hour") + solver.plural(int(minutes), "minute") + solver.plural(int(seconds), "second")
} else if minutes > 0 {
result = solver.plural(int(minutes), "minute") + solver.plural(int(seconds), "second")
} else {
result = solver.plural(int(seconds), "second")
}
return
}

View File

@ -1,26 +1,39 @@
package solver
import (
"sync/atomic"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// Solve a given Sudoku puzzle by iterating through all possible solutions.
type Solver struct {
blocks []int
row1 string
row2 string
row3 string
row4 string
row5 string
row6 string
row7 string
row8 string
row9 string
row1s []int
row2s []int
row3s []int
row4s []int
row5s []int
row6s []int
row7s []int
row8s []int
row9s []int
iter int64
counter int64
solutions []string
Controller *controller.Controller
// Slice of possible blocks for the 1st row.
row1s []string
// Slice of possible blocks for the 2nd row.
row2s []string
// Slice of possible blocks for the 3rd row.
row3s []string
// Slice of possible blocks for the 4th row.
row4s []string
// Slice of possible blocks for the 5th row.
row5s []string
// Slice of possible blocks for the 6th row.
row6s []string
// Slice of possible blocks for the 7th row.
row7s []string
// Slice of possible blocks for the 8th row.
row8s []string
// Slice of possible blocks for the 9th row.
row9s []string
// Maximum number of possible solutions with the current set of rows.
Iter uint64
// Progress counter, needs atomic due to the number of updates.
counter atomic.Uint64
// Slice of rates for accurate duration estimation.
rates []uint64
// Reference to Outputter interface
Outp *outputter.Outputter
}

View File

@ -0,0 +1,47 @@
package solver
// Validate combination
func (solver *Solver) validateCombination(row1 string, row2 string, row3 string, row4 string, row5 string, row6 string, row7 string, row8 string, row9 string) (retval bool) {
retval = true
for index := range 9 {
if row1[index] == row2[index] || row1[index] == row3[index] || row1[index] == row4[index] || row1[index] == row5[index] || row1[index] == row6[index] || row1[index] == row7[index] || row1[index] == row8[index] || row1[index] == row9[index] {
retval = false
}
if row2[index] == row1[index] || row2[index] == row3[index] || row2[index] == row4[index] || row2[index] == row5[index] || row2[index] == row6[index] || row2[index] == row7[index] || row2[index] == row8[index] || row2[index] == row9[index] {
retval = false
}
if row3[index] == row1[index] || row3[index] == row2[index] || row3[index] == row4[index] || row3[index] == row5[index] || row3[index] == row6[index] || row3[index] == row7[index] || row3[index] == row8[index] || row3[index] == row9[index] {
retval = false
}
if row4[index] == row1[index] || row4[index] == row2[index] || row4[index] == row3[index] || row4[index] == row5[index] || row4[index] == row6[index] || row4[index] == row7[index] || row4[index] == row8[index] || row4[index] == row9[index] {
retval = false
}
if row5[index] == row1[index] || row5[index] == row2[index] || row5[index] == row3[index] || row5[index] == row4[index] || row5[index] == row6[index] || row5[index] == row7[index] || row5[index] == row8[index] || row5[index] == row9[index] {
retval = false
}
if row6[index] == row1[index] || row6[index] == row2[index] || row6[index] == row3[index] || row6[index] == row4[index] || row6[index] == row5[index] || row6[index] == row7[index] || row6[index] == row8[index] || row6[index] == row9[index] {
retval = false
}
if row7[index] == row1[index] || row7[index] == row2[index] || row7[index] == row3[index] || row7[index] == row4[index] || row5[index] == row6[index] || row7[index] == row6[index] || row7[index] == row8[index] || row7[index] == row9[index] {
retval = false
}
if row8[index] == row1[index] || row8[index] == row2[index] || row8[index] == row3[index] || row8[index] == row4[index] || row8[index] == row5[index] || row8[index] == row6[index] || row8[index] == row7[index] || row8[index] == row9[index] {
retval = false
}
if row9[index] == row1[index] || row9[index] == row2[index] || row9[index] == row3[index] || row9[index] == row4[index] || row9[index] == row5[index] || row9[index] == row6[index] || row9[index] == row7[index] || row9[index] == row8[index] {
retval = false
}
}
return retval
}

View File

@ -0,0 +1,16 @@
package solver
import (
"testing"
)
// TestValidateCombination tests the validateCombination function.
func TestValidateCombination(t *testing.T) {
// Create a new solver
solver := Solver{}
// Test the validateCombination function
if !solver.validateCombination("769154832", "154832769", "832769154", "671945328", "945328671", "328671945", "597416283", "416283597", "283597416") {
t.Error("validateCombination failed")
}
}

12
solver/validator.go Normal file
View File

@ -0,0 +1,12 @@
package solver
// Validate the provided rows and verify it is a valid solution.
func (solver *Solver) validator(rows1Index int, rows2Index int, rows3Index int, rows4Index int, rows5Index int, rows6Index int, rows7Index int, rows8Index int, rows9Index int) {
solver.counter.Add(1)
if solver.validateCombination(solver.row1s[rows1Index], solver.row2s[rows2Index], solver.row3s[rows3Index], solver.row4s[rows4Index], solver.row5s[rows5Index], solver.row6s[rows6Index], solver.row7s[rows7Index], solver.row8s[rows8Index], solver.row9s[rows9Index]) {
solver.Controller.Solutions = append(solver.Controller.Solutions, []string{solver.row1s[rows1Index], solver.row2s[rows2Index], solver.row3s[rows3Index], solver.row4s[rows4Index], solver.row5s[rows5Index], solver.row6s[rows6Index], solver.row7s[rows7Index], solver.row8s[rows8Index], solver.row9s[rows9Index]})
}
}

53
solver/validator_test.go Normal file
View File

@ -0,0 +1,53 @@
package solver
import (
"testing"
"gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
)
// This function will test the validator function.
// It will create all the structs, and set required values
// Load the blocks, populate the blocks, and then run the validator
func TestValidator(t *testing.T) {
// Do all the necesarry steps to initialize the solver.
solver := Solver{}
outp := outputter.Outputter{}
outp.OutputType = "short"
solver.Outp = &outp
controller := controller.Controller{}
solver.Controller = &controller
solver.LoadBlocks()
// Fill the slices of solver.rows with the following values:
// [769154832 154832769 832769154 671945328 945328671 328671945 597416283 416283597 283597416]
solver.row1s = []string{"769154832"}
solver.row2s = []string{"154832769"}
solver.row3s = []string{"832769154"}
solver.row4s = []string{"671945328"}
solver.row5s = []string{"945328671"}
solver.row6s = []string{"328671945"}
solver.row7s = []string{"597416283"}
solver.row8s = []string{"416283597"}
solver.row9s = []string{"283597416"}
// Set the rows to validate
rows1Index := 0
rows2Index := 0
rows3Index := 0
rows4Index := 0
rows5Index := 0
rows6Index := 0
rows7Index := 0
rows8Index := 0
rows9Index := 0
// Run the validator
solver.validator(rows1Index, rows2Index, rows3Index, rows4Index, rows5Index, rows6Index, rows7Index, rows8Index, rows9Index)
// Check the number of solutions
if len(controller.Solutions) != 1 {
t.Errorf("Expected 1 solution, got %d", len(controller.Solutions))
}
}

View File

@ -1,94 +0,0 @@
// Processing
func (solver *Solver) routine_row1(index1 int) {
for index2 := range solver.row2s {
go solver.routine_row2(index1, index2)
}
}
func (solver *Solver) routine_row2(index1 int, index2 int) {
for index3 := range solver.row3s {
go solver.routine_row3(index1, index2, index3)
}
}
func (solver *Solver) routine_row3(index1 int, index2 int, index3 int) {
for index4 := range solver.row4s {
go solver.routine_row4(index1, index2, index3, index4)
}
}
func (solver *Solver) routine_row4(index1 int, index2 int, index3 int, index4 int) {
for index5 := range solver.row5s {
go solver.routine_row5(index1, index2, index3, index4, index5)
}
}
func (solver *Solver) routine_row5(index1 int, index2 int, index3 int, index4 int, index5 int) {
for index6 := range solver.row6s {
go solver.routine_row6(index1, index2, index3, index4, index5, index6)
}
}
func (solver *Solver) routine_row6(index1 int, index2 int, index3 int, index4 int, index5 int, index6 int) {
for index7 := range solver.row7s {
go solver.routine_row7(index1, index2, index3, index4, index5, index6, index7)
}
}
func (solver *Solver) routine_row7(index1 int, index2 int, index3 int, index4 int, index5 int, index6 int, index7 int) {
for index8 := range solver.row8s {
go solver.routine_row8(index1, index2, index3, index4, index5, index6, index7, index8)
}
}
func (solver *Solver) routine_row8(index1 int, index2 int, index3 int, index4 int, index5 int, index6 int, index7 int, index8 int) {
for index9 := range solver.row9s {
go solver.routine_row9(index1, index2, index3, index4, index5, index6, index7, index8, index9)
}
}
func (solver *Solver) routine_row9(index1 int, index2 int, index3 int, index4 int, index5 int, index6 int, index7 int, index8 int, index9 int) {
go solver.routine_validator(index1, index2, index3, index4, index5, index6, index7, index8, index9)
}
// blocks.go
func (solver *Solver) generate_blocks() []int {
var blocks []int
decvals := [9]int{49, 50, 51, 52, 53, 54, 55, 56, 57}
for counter := 123456789; counter <= 987654321; counter++ {
// Convert number to string ([]byte)
digits := strconv.Itoa(counter)
// Check if every number is only represented only once
var valid bool
valid = true
for decval := range decvals {
var count int
for digit := range digits {
if digits[digit] == byte(decvals[decval]) {
count = count + 1
}
}
if count != 1 {
valid = false
}
}
if valid {
blocks = append(blocks, counter)
}
}
return blocks
}
// stash.go
func (solver *Solver) print_block(block int) {
digits := strconv.Itoa(block)
fmt.Printf("%c %c %c\n%c %c %c\n%c %c %c\n\n", digits[0], digits[1], digits[2], digits[3], digits[4], digits[5], digits[6], digits[7], digits[8])
}