Skip to content

Commit

Permalink
Feature/sudoku pkg (#2)
Browse files Browse the repository at this point in the history
* wip sudoku engine; still buggy for expert puzzle

* engine update; still buggy expert mode

* better expert support with re-attempts after 3 sec
  • Loading branch information
fedeztk committed Sep 2, 2022
1 parent 51a2162 commit d8e4f6a
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 27 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@

# Dependency directories (remove the comment below to include it)
# vendor/

sku
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Simple TUI written in go to play sudoku in the terminal
[Features](#org26baa6c) -
[Testing](#org2744438)

sku is a simple TUI for playing sudoku inside the terminal. It uses the awesome [bubbletea](https://github.com/charmbracelet/bubbletea) TUI library for the UI and the [sudoku-go](https://github.com/forfuns/sudoku-go) library as the sudoku logic backend.
sku is a simple TUI for playing sudoku inside the terminal. It uses the awesome [bubbletea](https://github.com/charmbracelet/bubbletea) TUI library for the UI.

Screenshots [here](#org26baa6c)

Expand Down
2 changes: 1 addition & 1 deletion cmd/sku/.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
r5.a81afe3
r6.971f844
11 changes: 6 additions & 5 deletions cmd/sku/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

tea "github.com/charmbracelet/bubbletea"
model "github.com/fedeztk/sku/internal/model"
"github.com/fedeztk/sku/pkg/sudoku"
)

const (
Expand All @@ -23,14 +24,14 @@ var skuVersion string
func main() {
var mode int
modesMap := map[string]int{
"easy": LEVEL_EASY,
"medium": LEVEL_MEDIUM,
"hard": LEVEL_HARD,
"expert": LEVEL_EXPERT,
"easy": sudoku.EASY,
"medium": sudoku.MEDIUM,
"hard": sudoku.HARD,
"expert": sudoku.EXPERT,
}

if len(os.Args) < 2 {
mode = LEVEL_EASY
mode = sudoku.EASY
} else {
if os.Args[1] == "-v" || os.Args[1] == "--version" {
fmt.Println(skuVersion)
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ require github.com/charmbracelet/lipgloss v0.5.0
require (
github.com/charmbracelet/bubbles v0.10.3
github.com/containerd/console v1.0.3 // indirect
github.com/forfuns/sudoku-go v0.0.0-20220126091642-55eca2ac8dab
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/forfuns/sudoku-go v0.0.0-20220126091642-55eca2ac8dab h1:CrPsGMJUUyiQoVUqrARjwGeN+5bsKEcPliO3/H7tFMk=
github.com/forfuns/sudoku-go v0.0.0-20220126091642-55eca2ac8dab/go.mod h1:b/GpzuvP/0YPUWc0+ctCUrhZL2PX8RWlBfbTn8CRLEI=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
Expand Down
32 changes: 16 additions & 16 deletions internal/model/components/board/board.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/fedeztk/sku/internal/model/components/keys"
sudoku_generator "github.com/forfuns/sudoku-go/generator"
"github.com/fedeztk/sku/pkg/sudoku"
)

type Model struct {
board [sudoku_len][sudoku_len]struct {
puzzle int8
answer int8
puzzle int
answer int
modifiable bool
}
KeyMap keys.KeyMap
Expand Down Expand Up @@ -41,19 +41,19 @@ const (
func NewModel(mode int) Model {
var cellsLeft int
var board [sudoku_len][sudoku_len]struct {
puzzle int8
answer int8
puzzle int
answer int
modifiable bool
}

game, _ := sudoku_generator.Generate(mode)
puzzle, answer := game.Puzzle(), game.Answer()
sudoku := sudoku.New(mode)
puzzle, answer := sudoku.Puzzle, sudoku.Answer

for i := 0; i < sudoku_len; i++ {
for j := 0; j < sudoku_len; j++ {
board[i][j].puzzle = puzzle[i*sudoku_len+j]
board[i][j].answer = answer[i*sudoku_len+j]
if modifiable := puzzle[i*sudoku_len+j] == -1; modifiable {
if modifiable := puzzle[i*sudoku_len+j] == 0; modifiable {
board[i][j].modifiable = modifiable
cellsLeft++
}
Expand Down Expand Up @@ -83,7 +83,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case key.Matches(msg, m.KeyMap.Clear):
m.clear(m.cursor.row, m.cursor.col)
case key.Matches(msg, m.KeyMap.Number):
return m, m.set(m.cursor.row, m.cursor.col, int8(msg.String()[0]-'0'))
return m, m.set(m.cursor.row, m.cursor.col, int(msg.String()[0]-'0'))
}
case gameCheck:
m.Err = msg.Err
Expand All @@ -100,9 +100,9 @@ func (m Model) Init() tea.Cmd {
}

func (m Model) View() string {
// replace -1 with empty string
var maybeReplace = func(v int8) string {
if v == -1 {
// replace 0 with empty string
var maybeReplace = func(v int) string {
if v == 0 {
return " "
}
return fmt.Sprintf("%d", v)
Expand All @@ -123,9 +123,9 @@ func (m Model) View() string {
return boardView + cellsLeftStyle.Render(fmt.Sprintf("Cells left: %d", m.cellsLeft))
}

func (m *Model) set(row, col int, v int8) tea.Cmd {
func (m *Model) set(row, col int, v int) tea.Cmd {
if m.board[row][col].modifiable {
if m.board[row][col].puzzle == -1 {
if m.board[row][col].puzzle == 0 {
m.cellsLeft--
}
m.board[row][col].puzzle = v
Expand All @@ -141,10 +141,10 @@ func (m *Model) set(row, col int, v int8) tea.Cmd {

func (m *Model) clear(row, col int) {
if m.board[row][col].modifiable {
if m.board[row][col].puzzle != -1 {
if m.board[row][col].puzzle != 0 {
m.cellsLeft++
}
m.board[row][col].puzzle = -1
m.board[row][col].puzzle = 0

delete(m.errCoordinates, coordinate{row, col})
}
Expand Down
21 changes: 21 additions & 0 deletions pkg/sudoku/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Sudoku Engine
This package is a **non** efficient implementation of a sudoku solver and generator. It is ~~stolen~~ ported from [this python implementation](https://www.101computing.net/sudoku-generator-algorithm/), all credits go to the author. It is a simple recursive and backtracking engine, easy to understand and read.

## Motivation
This minimal implementation was done due to the lack of a package that simply has the two sensible features needed by a sudoku engine:
1. can both solve & generate
2. do generate a one-solution puzzle

without useless features or generally cumbersome development experience (e.g. overly complicated structures, interfaces, initialization methods, etc).

If you need the aforementioned features this package is not the right one, since it only does point 1 and 2.

If you need a super efficient implementation this package is not the right one since it is aimed to be simple at the expense of speed; if you need something like that you can use/write one that leverages a more performant algorithm such as dancing links (dlx).

## Usage
```go
sudoku := sudoku.New(sudoku.EASY) // also available: MEDIUM, HARD, EXPERT

fmt.Println(sudoku.Puzzle) // unsolved sudoku [81]int
fmt.Println(sudoku.Answer) // solved sudoku [81]int
```
147 changes: 147 additions & 0 deletions pkg/sudoku/sudoku.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package sudoku

import (
"math/rand"
"time"
)

const (
// available sudoku difficulties
EASY = 42
MEDIUM = 36
HARD = 27
EXPERT = 25
// time constraints
MAX_ATTEMPTS = 5
MAX_EXEC_TIME = 3
// sudoku board size
SUDOKU_LENGTH = 9
SUDOKU_SIZE = SUDOKU_LENGTH * SUDOKU_LENGTH
)

type Sudoku struct {
Puzzle [SUDOKU_SIZE]int
Answer [SUDOKU_SIZE]int
}

func New(difficulty int) *Sudoku {
for i := 0; i < MAX_ATTEMPTS; i++ {
if s, ok := newWithTimer(difficulty); ok {
return s
}
}
return nil
}

func newWithTimer(difficulty int) (*Sudoku, bool) {
s := &Sudoku{}

done := make(chan struct{})
var ok bool

go func() {
s = &Sudoku{}
fill(&s.Puzzle)
s.Answer = s.Puzzle
s.eraseSome(difficulty)
close(done)
}()

select {
case <-done:
ok = true
case <-time.After(time.Second * MAX_EXEC_TIME):
}

return s, ok
}

func (s *Sudoku) eraseSome(difficulty int) {
for erased := 0; SUDOKU_SIZE-erased > difficulty; {
idx := 0
for s.Puzzle[idx] == 0 {
rand.Seed(time.Now().UnixNano())
idx = rand.Intn(SUDOKU_SIZE)
}

copyGrid := s.Puzzle
copyGrid[idx] = 0

count := 0
solve(&copyGrid, &count)
if count == 1 {
s.Puzzle[idx] = 0
erased++
}
}
}

func solve(grid *[SUDOKU_SIZE]int, count *int) {
if *count > 1 { // no need to go further
return
}

for idx := 0; idx < SUDOKU_SIZE; idx++ {
if grid[idx] == 0 {
for n := 1; n <= SUDOKU_LENGTH; n++ {
if isValid(grid, idx, n) {
grid[idx] = n
if checkFull(grid) {
*count++
}
solve(grid, count)
grid[idx] = 0
}
}
break
}
}
}

func fill(grid *[SUDOKU_SIZE]int) bool {
numberList := [SUDOKU_LENGTH]int{1, 2, 3, 4, 5, 6, 7, 8, 9}

for idx := 0; idx < SUDOKU_SIZE; idx++ {
if grid[idx] == 0 {
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(numberList), func(i, j int) {
numberList[i], numberList[j] = numberList[j], numberList[i]
})

for _, n := range numberList {
if isValid(grid, idx, n) {
grid[idx] = n
if checkFull(grid) || fill(grid) {
return true
}
grid[idx] = 0
}
}
break
}
}
return false
}

func checkFull(grid *[SUDOKU_SIZE]int) bool {
for i := 0; i < SUDOKU_SIZE; i++ {
if grid[i] == 0 {
return false
}
}
return true
}

func isValid(grid *[SUDOKU_SIZE]int, idx, n int) bool {
// check if num is valid in row, col, and 3x3 box
row := idx / SUDOKU_LENGTH
col := idx % SUDOKU_LENGTH
for i := 0; i < SUDOKU_LENGTH; i++ {
if grid[row*SUDOKU_LENGTH+i] == n ||
grid[i*SUDOKU_LENGTH+col] == n ||
grid[((row/3)*3+i/3)*SUDOKU_LENGTH+((col/3)*3+i%3)] == n {
return false
}
}
return true
}
45 changes: 45 additions & 0 deletions pkg/sudoku/sudoku_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package sudoku

import (
"fmt"
"testing"
)

func TestSudoku(t *testing.T) {
t.Log("TestSudoku")
var sudoku Sudoku
modes := map[int]string{
EASY: "easy",
MEDIUM: "medium",
HARD: "hard",
EXPERT: "expert",
}

for d, mode := range modes {
t.Log("Mode:", mode)
sudoku = *New(d)
t.Log(printSudoku(&sudoku))
}
}

func printSudoku(s *Sudoku) string {
board := "\n"
for i := 0; i < 81; i = i + 9 {
board += fmt.Sprintf("%d\n", s.Puzzle[i:i+9])
}
board += "\n"
for i := 0; i < 81; i = i + 9 {
board += fmt.Sprintf("%d\n", s.Answer[i:i+9])
}
notErased := 0
for i := 0; i < 81; i++ {
if s.Puzzle[i] == 0 {
board += "."
} else {
board += fmt.Sprintf("%d", s.Puzzle[i])
notErased++
}
}
board += fmt.Sprintf("\nNot erased: %d", notErased)
return board
}
2 changes: 1 addition & 1 deletion template/archlinux/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

pkgname=sku-git
_name=sku
pkgver=r5.a81afe3
pkgver=r6.971f844
pkgrel=1
pkgdesc="Simple TUI sudoku written in go"
arch=('any')
Expand Down

0 comments on commit d8e4f6a

Please sign in to comment.