Skip to content

Commit 0a7e677

Browse files
authored
feat: Add sudoku game (#18)
1 parent 7fc5813 commit 0a7e677

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

cmd/gg/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/Kaamkiya/gg/internal/app/maze"
1010
"github.com/Kaamkiya/gg/internal/app/pong"
1111
"github.com/Kaamkiya/gg/internal/app/snake"
12+
"github.com/Kaamkiya/gg/internal/app/sudoku"
1213
"github.com/Kaamkiya/gg/internal/app/tictactoe"
1314
"github.com/Kaamkiya/gg/internal/app/twenty48"
1415

@@ -24,6 +25,7 @@ func main() {
2425
Title("choose a game:").
2526
Options(
2627
huh.NewOption("2048", "twenty48"),
28+
huh.NewOption("sudoku", "sudoku"),
2729
huh.NewOption("dodger", "dodger"),
2830
huh.NewOption("maze", "maze"),
2931
huh.NewOption("hangman", "hangman"),
@@ -59,6 +61,8 @@ func main() {
5961
connect4.Run()
6062
case "snake":
6163
snake.Run()
64+
case "sudoku":
65+
sudoku.Run()
6266
default:
6367
panic("This game either doesn't exist or hasn't been implemented.")
6468
}

internal/app/sudoku/sudoku.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package sudoku
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/Kaamkiya/gg/internal/app/sudoku/sudokugenerator"
8+
9+
tea "github.com/charmbracelet/bubbletea"
10+
"github.com/charmbracelet/lipgloss"
11+
)
12+
13+
type model struct {
14+
origGrid [][]int
15+
grid [][]int
16+
17+
cursorx int
18+
cursory int
19+
}
20+
21+
func (m model) Init() tea.Cmd {
22+
return nil
23+
}
24+
25+
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
26+
switch msg := msg.(type) {
27+
case tea.KeyMsg:
28+
switch msg.String() {
29+
case "q", "ctrl+c":
30+
return m, tea.Quit
31+
case "up", "k":
32+
if m.cursory > 0 {
33+
m.cursory--
34+
}
35+
case "down", "j":
36+
if m.cursory < 8 {
37+
m.cursory++
38+
}
39+
case "left", "h":
40+
if m.cursorx > 0 {
41+
m.cursorx--
42+
}
43+
case "right", "l":
44+
if m.cursorx < 8 {
45+
m.cursorx++
46+
}
47+
case "1", "2", "3", "4", "5", "6", "7", "8", "9", "0":
48+
m.setSquare(msg.String())
49+
}
50+
}
51+
52+
return m, nil
53+
}
54+
55+
func (m model) View() string {
56+
s := ""
57+
58+
for i, r := range m.grid {
59+
for j, c := range r {
60+
if j%3 == 0 && j != 0 {
61+
s += " | "
62+
}
63+
64+
if j == m.cursorx && i == m.cursory {
65+
col := lipgloss.NewStyle().Background(lipgloss.Color("#0000ff")).Render
66+
if c == 0 {
67+
s += col(" . ")
68+
} else {
69+
s += col(fmt.Sprintf(" %d ", c))
70+
}
71+
} else {
72+
if c == 0 {
73+
s += " . "
74+
} else {
75+
s += fmt.Sprintf(" %d ", c)
76+
}
77+
}
78+
}
79+
80+
s += "\n"
81+
82+
if i == 2 || i == 5 {
83+
s += "--------------------------------\n"
84+
}
85+
}
86+
87+
s += fmt.Sprintf("\n\norig: %v\n\ncurr: %v", m.origGrid, m.grid)
88+
89+
return s
90+
}
91+
92+
func (m *model) setSquare(button string) {
93+
if m.origGrid[m.cursory][m.cursorx] == 0 {
94+
m.grid[m.cursory][m.cursorx], _ = strconv.Atoi(button)
95+
}
96+
}
97+
98+
func initialModel() tea.Model {
99+
g := sudokugenerator.Model{}
100+
g.Init()
101+
102+
grid := make([][]int, 9)
103+
orig := make([][]int, 9)
104+
105+
for i := range 9 {
106+
grid[i] = make([]int, 9)
107+
orig[i] = make([]int, 9)
108+
109+
for j := range 9 {
110+
grid[i][j] = g.Grid[j][i]
111+
orig[i][j] = g.Grid[j][i]
112+
}
113+
}
114+
115+
return model{
116+
grid: grid,
117+
origGrid: orig,
118+
}
119+
}
120+
121+
func Run() {
122+
p := tea.NewProgram(initialModel())
123+
124+
if _, err := p.Run(); err != nil {
125+
panic(err)
126+
}
127+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package sudokugenerator
2+
3+
import (
4+
"math/rand/v2"
5+
"slices"
6+
)
7+
8+
type Model struct {
9+
Grid [][]int
10+
}
11+
12+
func (m *Model) unusedInBox(row, col, n int) bool {
13+
for i := range 3 {
14+
for j := range 3 {
15+
if m.Grid[row+i][col+j] == n {
16+
return false
17+
}
18+
}
19+
}
20+
21+
return true
22+
}
23+
24+
func (m *Model) fillBox(row, col int) {
25+
var n int
26+
for i := range 3 {
27+
for j := range 3 {
28+
for !m.unusedInBox(row, col, n) {
29+
n = rand.IntN(9) + 1
30+
}
31+
m.Grid[row+i][col+j] = n
32+
}
33+
}
34+
}
35+
36+
func (m *Model) unusedInCol(col, n int) bool {
37+
for j := range 9 {
38+
if m.Grid[j][col] == n {
39+
return false
40+
}
41+
}
42+
43+
return true
44+
}
45+
46+
func (m *Model) unusedInRow(row, n int) bool {
47+
return !slices.Contains(m.Grid[row], n)
48+
}
49+
50+
func (m *Model) isSafe(row, col, n int) bool {
51+
return m.unusedInBox(row-row%3, col-col%3, n) && m.unusedInCol(col, n) && m.unusedInRow(row, n)
52+
}
53+
54+
func (m *Model) fillRemaining(row, col int) bool {
55+
if row == 9 {
56+
return true
57+
}
58+
59+
if col == 9 {
60+
return m.fillRemaining(row+1, 0)
61+
}
62+
63+
if m.Grid[row][col] != 0 {
64+
return m.fillRemaining(row, col+1)
65+
}
66+
67+
for n := 1; n < 10; n++ {
68+
if m.isSafe(row, col, n) {
69+
m.Grid[row][col] = n
70+
if m.fillRemaining(row, col+1) {
71+
return true
72+
}
73+
m.Grid[row][col] = 0
74+
}
75+
}
76+
77+
return false
78+
}
79+
80+
func (m *Model) emptyCells(amount int) {
81+
for amount > 0 {
82+
id := rand.IntN(81)
83+
i := id / 9
84+
j := id % 9
85+
86+
if m.Grid[i][j] != 0 {
87+
m.Grid[i][j] = 0
88+
amount--
89+
}
90+
}
91+
}
92+
93+
func (m *Model) generate() {
94+
for i := 0; i < 9; i += 3 {
95+
m.fillBox(i, i)
96+
}
97+
98+
m.fillRemaining(0, 0)
99+
}
100+
101+
func (m *Model) Init() {
102+
m.Grid = make([][]int, 9)
103+
for i := range m.Grid {
104+
m.Grid[i] = make([]int, 9)
105+
}
106+
107+
m.generate()
108+
m.emptyCells(54)
109+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package sudokugenerator
2+
3+
import "testing"
4+
5+
func TestGen(t *testing.T) {
6+
m := Model{}
7+
m.Init()
8+
9+
m.grid = make([][]int, 9)
10+
for i := range m.grid {
11+
m.grid[i] = make([]int, 9)
12+
}
13+
m.generate()
14+
15+
for r, row := range m.grid {
16+
for c, cell := range row {
17+
if !m.unusedInBox(r-r%3, c-c%3, cell) || !m.unusedInCol(c, cell) || !m.unusedInRow(r, cell) {
18+
t.Fatalf("Invalid Sudoku generated: %d overlaps", cell)
19+
}
20+
}
21+
}
22+
23+
m.emptyCells(20)
24+
c := 0
25+
for _, r := range m.grid {
26+
for _, n := range r {
27+
if n == 0 {
28+
c++
29+
}
30+
}
31+
}
32+
33+
if c != 20 {
34+
t.Fatalf("Not enough empty cells: wanted=20 got=%d", c)
35+
}
36+
}

0 commit comments

Comments
 (0)