Compare commits

6 Commits

35 changed files with 902 additions and 120 deletions

View File

@ -15,13 +15,17 @@ I wrote [a blog post](https://blog.ligthert.net/posts/exploration-fun-and-proces
* Split workloads among several computers * Split workloads among several computers
## Usage ## Usage
To use the sudoku solver, run the binary with all the parameters available: 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: Usage of ./sudoku-funpark:
-numcpu int -numcpu int
Number of CPU cores to assign to this task. (default 12) 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 -part int
Process part x in n parts. Cannot be lower than 1, or higher than specified in split. (default 1) 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 -row1 string
1st row of the sudoku puzzle. (default "000000000") 1st row of the sudoku puzzle. (default "000000000")
-row2 string -row2 string
@ -50,7 +54,7 @@ Instead of using the 3x3 blocks with 3x3 digits, it uses horizontal rows from to
## Example ## Example
To see the solver in action, run the tool with the following parameters. To see the solver in action, run the tool with the following parameters.
For a short running (~15 seconds) example: 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 > $ ./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: For a long running (~1 hours 15 minutes) example:
@ -59,39 +63,39 @@ For a long running (~1 hours 15 minutes) example:
The outpot (of the short running parameters) will look something like this: 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 ./sudoku-funpark -row1 769104802 -row2 154800060 -row3 832700154 -row4 600900328 -row5 045328670 -row6 328670945 -row7 597410280 -row8 006283090 -row9 200590006
2025/01/24 00:05:58 Loading blocks Loading blocks... Done! (38.957376ms)
2025/01/24 00:05:58 Loaded blocks (34.587221ms) Populating blocks... Done! (92.087174ms)
2025/01/24 00:05:58 Populating blocks Number of (potential) solutions: 26542080
2025/01/24 00:05:58 Populated blocks (438.73054ms) Validating solutions
2025/01/24 00:05:58 Number of (potential) solutions: 26542080 Processing: 8% (2131893/26542080); Rate: 2131884/sec for 1.000028115s; Time left (est.): 11s
2025/01/24 00:05:58 Validating solutions Processing: 16% (4292163/26542080); Rate: 2160219/sec for 1.000087826s; Time left (est.): 10s
2025/01/24 00:05:59 Processing: 6% (1729332/26542080); Rate (avg): 1729330/sec for 1.000003166s; Time left (est.): 14 seconds Processing: 24% (6438334/26542080); Rate: 2146157/sec for 1.000017364s; Time left (est.): 9s
2025/01/24 00:06:00 Processing: 13% (3461753/26542080); Rate (avg): 1732418/sec for 1.000002285s; Time left (est.): 13 seconds Processing: 32% (8529362/26542080); Rate: 2090965/sec for 1.000367121s; Time left (est.): 8s
2025/01/24 00:06:01 Processing: 19% (5228965/26542080); Rate (avg): 1767215/sec for 1.000019297s; Time left (est.): 12 seconds Processing: 40% (10737065/26542080); Rate: 2207530/sec for 1.000072427s; Time left (est.): 7s
2025/01/24 00:06:02 Processing: 26% (6996958/26542080); Rate (avg): 1767992/sec for 1.000200176s; Time left (est.): 11 seconds Processing: 48% (12958905/26542080); Rate: 2221755/sec for 1.000003187s; Time left (est.): 6s
2025/01/24 00:06:03 Processing: 33% (8767450/26542080); Rate (avg): 1770495/sec for 1.000016352s; Time left (est.): 10 seconds Processing: 57% (15163877/26542080); Rate: 2204929/sec for 1.000002717s; Time left (est.): 5s
2025/01/24 00:06:04 Processing: 39% (10576900/26542080); Rate (avg): 1809450/sec for 1.000014638s; Time left (est.): 8 seconds Processing: 65% (17254760/26542080); Rate: 2090742/sec for 1.00008452s; Time left (est.): 4s
2025/01/24 00:06:05 Processing: 46% (12400058/26542080); Rate (avg): 1823158/sec for 1.000352862s; Time left (est.): 7 seconds Processing: 73% (19513142/26542080); Rate: 2258348/sec for 1.000071076s; Time left (est.): 3s
2025/01/24 00:06:06 Processing: 53% (14185155/26542080); Rate (avg): 1785095/sec for 1.000254888s; Time left (est.): 6 seconds Processing: 82% (21795213/26542080); Rate: 2282028/sec for 1.000076024s; Time left (est.): 2s
2025/01/24 00:06:07 Processing: 60% (15968402/26542080); Rate (avg): 1783245/sec for 1.000002305s; Time left (est.): 5 seconds Processing: 90% (24048891/26542080); Rate: 2253645/sec for 1.000146957s; Time left (est.): 1s
2025/01/24 00:06:08 Processing: 66% (17655770/26542080); Rate (avg): 1687370/sec for 1.000068309s; Time left (est.): 5 seconds Processing: 98% (26226252/26542080); Rate: 2177215/sec for 1.000129955s; Time left (est.): 0s
2025/01/24 00:06:09 Processing: 73% (19442885/26542080); Rate (avg): 1787111/sec for 1.000006984s; Time left (est.): 3 seconds Processing: 100% (26542080/26542080); Rate: 315792/sec for 1.000105149s; Time left (est.): 0s
2025/01/24 00:06:10 Processing: 79% (21183545/26542080); Rate (avg): 1740661/sec for 1.000002395s; Time left (est.): 3 seconds Validated solutions (13.001683829s)
2025/01/24 00:06:11 Processing: 86% (22998945/26542080); Rate (avg): 1815402/sec for 1.000113534s; Time left (est.): 1 second
2025/01/24 00:06:12 Processing: 90% (24109203/26542080); Rate (avg): 1110261/sec for 1.000312346s; Time left (est.): 2 seconds
2025/01/24 00:06:13 Processing: 100% (26542080/26542080); Rate (avg): 0/sec for 1.000117421s; Time left (est.): N/A
2025/01/24 00:06:13 Validated solutions (15.002654066s)
2025/01/24 00:06:13
Solution #1: Solution #1:
769154832 ╔═══════════╗
154832769 ║769│154832
832769154 ║154│832769
671945328 ║832│769│154╢
945328671 ╟───┼───┼───╢
328671945 671945│328╢
597416283 ║945│328│671╢
416283597 ║328│671│945╢
283597416 ╟───┼───┼───╢
║597│416│283╢
║416│283│597╢
║283│597│416╢
╚═══════════╝
``` ```
## Caveats ## Caveats

View File

@ -1,5 +1,7 @@
package controller package controller
import "gitea.ligthert.net/golang/sudoku-funpark/outputter"
// Simple interface to store values shared amongst packages. // Simple interface to store values shared amongst packages.
type Controller struct { type Controller struct {
// All possible blocks/rows available // All possible blocks/rows available
@ -32,4 +34,8 @@ type Controller struct {
Part int Part int
// Type of output requested // Type of output requested
Output string 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)
}
}

View File

@ -1,14 +0,0 @@
package export
import (
"fmt"
"log"
)
// Print solutions into a human friendly format for in the console.
func (export *Export) PrintFlatSolutions() {
for solutionIndex, solution := range export.Controller.Solutions {
log.Printf("\nSolution #%d:", solutionIndex+1)
fmt.Println(solution)
}
}

View File

@ -1,21 +0,0 @@
package export
import (
"fmt"
"log"
)
// Print solutions into a human friendly format for in the console.
func (export *Export) PrintHumanReadableSolutions() {
for solutionIndex, solution := range export.Controller.Solutions {
log.Printf("\nSolution #%d:", solutionIndex+1)
fmt.Println("╔═══════════╗")
for rowIndex, row := range solution {
if rowIndex == 3 || rowIndex == 6 {
fmt.Println("╟───┼───┼───╢")
}
fmt.Println("║" + row[0:3] + "│" + row[3:6] + "│" + row[6:9] + "╢")
}
fmt.Println("╚═══════════╝")
}
}

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)
}
}

View File

@ -2,7 +2,7 @@ package flags
import ( import (
"flag" "flag"
"log" "fmt"
"os" "os"
"runtime" "runtime"
"strings" "strings"
@ -24,14 +24,15 @@ func (flags *Flags) ParseFlags() {
flag.IntVar(&flags.Controller.NumCPUs, "numcpu", runtime.NumCPU(), "Number of CPU cores to assign to this task.") 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.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.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.") 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 // Parse the flags
flag.Parse() flag.Parse()
// Process any changes to the CPU usage. // Process any changes to the CPU usage.
if flags.Controller.NumCPUs <= 0 { if flags.Controller.NumCPUs <= 0 {
log.Printf("ERROR: Number of CPU cores must be 1 or higher.\n\n") fmt.Printf("ERROR: Number of CPU cores must be 1 or higher.\n\n")
flags.printUsage() flags.printUsage()
os.Exit(1) os.Exit(1)
} }
@ -42,7 +43,7 @@ func (flags *Flags) ParseFlags() {
// Process rows // 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" { 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" {
log.Printf("ERROR: All parameters must be entered.\n\n") fmt.Printf("ERROR: All parameters must be entered.\n\n")
flags.printUsage() flags.printUsage()
os.Exit(1) os.Exit(1)
} }
@ -61,22 +62,30 @@ func (flags *Flags) ParseFlags() {
// Process workload splitting // Process workload splitting
// Ensure split and part are 1 or higher // Ensure split and part are 1 or higher
if flags.Controller.Split <= 0 || flags.Controller.Part <= 0 { if flags.Controller.Split <= 0 || flags.Controller.Part <= 0 {
log.Printf("ERROR: '-split' and '-part' need to be 1 or higher.\n") fmt.Printf("ERROR: '-split' and '-part' need to be 1 or higher.\n")
flags.printUsage() flags.printUsage()
os.Exit(1) os.Exit(1)
} }
// Ensure part is between 1 and split // Ensure part is between 1 and split
if flags.Controller.Part > flags.Controller.Split { if flags.Controller.Part > flags.Controller.Split {
log.Printf("ERROR: '-part' cannot be bigger than `-split`.\n") fmt.Printf("ERROR: '-part' cannot be bigger than `-split`.\n")
flags.printUsage() flags.printUsage()
os.Exit(1) os.Exit(1)
} }
// Process output selection // Process output selection
flags.Controller.Output = strings.ToLower(flags.Controller.Output) flags.Controller.Output = strings.ToLower(flags.Controller.Output)
if flags.Controller.Output != "human" && flags.Controller.Output != "flat" { if flags.Controller.Output != "human" && flags.Controller.Output != "flat" && flags.Controller.Output != "json" {
log.Printf("ERROR: Invalid output, can only be 'human' or 'flat'.\n") 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() flags.printUsage()
os.Exit(1) os.Exit(1)
} }

View File

@ -6,19 +6,6 @@ import (
"os" "os"
) )
// 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
}
// Print help information for the end-user // Print help information for the end-user
func (flags *Flags) printUsage() { func (flags *Flags) printUsage() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])

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)
}
})
}
}

View File

@ -1,7 +1,7 @@
package flags package flags
import ( import (
"log" "fmt"
"os" "os"
) )
@ -18,7 +18,7 @@ func (flags *Flags) validateRow(name string, row string) {
// 1. Make sure the row is 9 in length // 1. Make sure the row is 9 in length
if len(row) != 9 { if len(row) != 9 {
log.Printf("ERROR: Invalid length of %s (%s), must be 9 numbers\n\n", name, row) fmt.Printf("ERROR: Invalid length of %s (%s), must be 9 numbers\n\n", name, row)
flags.printUsage() flags.printUsage()
os.Exit(1) os.Exit(1)
} }
@ -29,7 +29,7 @@ func (flags *Flags) validateRow(name string, row string) {
} }
if !found { if !found {
log.Printf("ERROR: Invalid character of %s (%s), must be 9 numbers\n\n", name, row) fmt.Printf("ERROR: Invalid character of %s (%s), must be 9 numbers\n\n", name, row)
flags.printUsage() flags.printUsage()
os.Exit(1) os.Exit(1)
} }
@ -46,7 +46,7 @@ func (flags *Flags) validateRow(name string, row string) {
} }
if double { if double {
log.Printf("ERROR: Double character of %s (%s), numbers between 1 and 9 may only be entered once\n\n", name, row) fmt.Printf("ERROR: Double character of %s (%s), numbers between 1 and 9 may only be entered once\n\n", name, row)
flags.printUsage() flags.printUsage()
os.Exit(1) os.Exit(1)
} }

21
main.go
View File

@ -1,29 +1,34 @@
package main package main
import ( import (
"log" "fmt"
"runtime" "runtime"
"strconv" "strconv"
"gitea.ligthert.net/golang/sudoku-funpark/controller" "gitea.ligthert.net/golang/sudoku-funpark/controller"
"gitea.ligthert.net/golang/sudoku-funpark/export" "gitea.ligthert.net/golang/sudoku-funpark/export"
"gitea.ligthert.net/golang/sudoku-funpark/flags" "gitea.ligthert.net/golang/sudoku-funpark/flags"
"gitea.ligthert.net/golang/sudoku-funpark/outputter"
"gitea.ligthert.net/golang/sudoku-funpark/solver" "gitea.ligthert.net/golang/sudoku-funpark/solver"
) )
func main() { func main() {
// Instantiate the interfaces // Instantiate the interfaces
controller := controller.Controller{} controller := controller.Controller{}
outp := outputter.Outputter{}
export := export.Export{Controller: &controller} export := export.Export{Controller: &controller}
flags := flags.Flags{Controller: &controller} flags := flags.Flags{Controller: &controller}
solver := solver.Solver{Controller: &controller} solver := solver.Solver{Controller: &controller, Outp: &outp}
// Parse and handle flags // Parse and handle flags
flags.ParseFlags() flags.ParseFlags()
// Tell outp what kind of output is expected.
outp.OutputType = controller.Print
// Report number of CPUs being used, if set. // Report number of CPUs being used, if set.
if runtime.NumCPU() != controller.NumCPUs { if runtime.NumCPU() != controller.NumCPUs {
log.Println("Using " + strconv.Itoa(controller.NumCPUs) + " CPUs, (was " + strconv.Itoa(runtime.NumCPU()) + ")") outp.Println("Using " + strconv.Itoa(controller.NumCPUs) + " CPUs, (was " + strconv.Itoa(runtime.NumCPU()) + ")")
} }
// Load blocks from CSV file // Load blocks from CSV file
@ -39,18 +44,12 @@ func main() {
} }
// Print the total number of solutions to validate // Print the total number of solutions to validate
log.Println("Number of (potential) solutions:", solver.Iter) outp.Println("Number of (potential) solutions:", solver.Iter)
// Check the number of solutions // Check the number of solutions
go solver.CheckCombinations() go solver.CheckCombinations()
solver.Tracker() solver.Tracker()
// Print the valid solutions fmt.Println(export.Export())
switch controller.Output {
case "human":
export.PrintHumanReadableSolutions()
case "flat":
export.PrintFlatSolutions()
}
} }

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

@ -2,7 +2,6 @@ package solver
import ( import (
"embed" "embed"
"log"
"strings" "strings"
"time" "time"
) )
@ -15,8 +14,8 @@ var f embed.FS
// Load all possible blocks from CSV in to memory // Load all possible blocks from CSV in to memory
func (solver *Solver) LoadBlocks() { func (solver *Solver) LoadBlocks() {
defer solver.timeTrack(time.Now(), "Loaded blocks") defer solver.timeTrack(time.Now(), "Done!")
log.Println("Loading blocks") solver.Outp.Printf("Loading blocks... ")
var blocks []string var blocks []string

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])
}
}

View File

@ -1,15 +1,14 @@
package solver package solver
import ( import (
"log"
"time" "time"
) )
// Find all possible blocks that can be used to find a solution. // Find all possible blocks that can be used to find a solution.
func (solver *Solver) PopulateBlocks() { func (solver *Solver) PopulateBlocks() {
defer solver.timeTrack(time.Now(), "Populated blocks") defer solver.timeTrack(time.Now(), "Done!")
log.Println("Populating blocks") solver.Outp.Printf("Populating blocks... ")
solver.findBlocks(&solver.Controller.Row1, &solver.row1s) solver.findBlocks(&solver.Controller.Row1, &solver.row1s)
solver.findBlocks(&solver.Controller.Row2, &solver.row2s) solver.findBlocks(&solver.Controller.Row2, &solver.row2s)

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)
}
}

View File

@ -1,7 +1,6 @@
package solver package solver
import ( import (
"log"
"os" "os"
"strconv" "strconv"
"time" "time"
@ -12,12 +11,12 @@ import (
// Modify solver.row1s so it limits the workload to what is only desired // Modify solver.row1s so it limits the workload to what is only desired
func (solver *Solver) SelectWorkload() { func (solver *Solver) SelectWorkload() {
if solver.Controller.Split > len(solver.row1s) { if solver.Controller.Split > len(solver.row1s) {
log.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") 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) os.Exit(1)
} }
defer solver.timeTrack(time.Now(), "Workload set") defer solver.timeTrack(time.Now(), "Workload set")
log.Println("Setting workload") solver.Outp.Println("Setting workload")
log.Println("We are agent " + strconv.Itoa(solver.Controller.Part) + " of " + strconv.Itoa(solver.Controller.Split)) solver.Outp.Println("We are agent " + strconv.Itoa(solver.Controller.Part) + " of " + strconv.Itoa(solver.Controller.Split))
workloads := solver.splitWorkload() workloads := solver.splitWorkload()
solver.setWorkload(workloads) solver.setWorkload(workloads)
} }

View File

@ -1,7 +1,6 @@
package solver package solver
import ( import (
"log"
"strconv" "strconv"
"time" "time"
) )
@ -12,7 +11,7 @@ func (solver *Solver) Tracker() {
// Add time tracking // Add time tracking
defer solver.timeTrack(time.Now(), "Validated solutions") defer solver.timeTrack(time.Now(), "Validated solutions")
log.Println("Validating solutions") solver.Outp.Println("Validating solutions")
// Determine if the main-loop is done // Determine if the main-loop is done
var done bool var done bool
@ -73,7 +72,7 @@ func (solver *Solver) Tracker() {
} }
// Printing the progress // Printing the progress
log.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) 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 // After we are done printing, exit this for-loop
if percentage == 100 { if percentage == 100 {

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())
}
}

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

@ -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

@ -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,7 +1,6 @@
package solver package solver
import ( import (
"log"
"time" "time"
) )
@ -9,5 +8,5 @@ import (
// Use with `defer` // Use with `defer`
func (solver *Solver) timeTrack(start time.Time, msg string) { func (solver *Solver) timeTrack(start time.Time, msg string) {
elapsed := time.Since(start) elapsed := time.Since(start)
log.Printf("%s (%s)", msg, elapsed) solver.Outp.Printf("%s (%s)\n", msg, elapsed)
} }

View File

@ -4,6 +4,7 @@ import (
"sync/atomic" "sync/atomic"
"gitea.ligthert.net/golang/sudoku-funpark/controller" "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. // Solve a given Sudoku puzzle by iterating through all possible solutions.
@ -33,4 +34,6 @@ type Solver struct {
counter atomic.Uint64 counter atomic.Uint64
// Slice of rates for accurate duration estimation. // Slice of rates for accurate duration estimation.
rates []uint64 rates []uint64
// Reference to Outputter interface
Outp *outputter.Outputter
} }

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")
}
}

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))
}
}