今天主要要講解
此外,數字必須介於 1 到 9 之間,0 代表空格。
// 檢查 row, col 位置是否可以放置 num
func (board *Board) isSafe(row, col, num int) bool {
// 檢查行與列是否有放相同的值
for i := 0; i < BoardSize; i++ {
if board.Cells[row][i].Value == num || board.Cells[i][col].Value == num {
return false
}
}
// 檢查 Box 內是否有相同的值
boxRow := (row / BoxSize) * BoxSize
boxCol := (col / BoxSize) * BoxSize
for rc := 0; rc < BoxSize; rc++ {
for bc := 0; bc < BoxSize; bc++ {
if board.Cells[boxRow+rc][boxCol+bc].Value == num {
return false
}
}
}
// 檢查 num 值介於 1 到 9
if num < 1 || num > 9 {
return false
}
return true
}
Backtracking(回溯演算法)常用於解決排列組合與棋盤類問題。其核心概念是「嘗試 → 檢查 → 回退」,適合用於 Sudoku 這類需要逐步填入數字並檢查合法性的題目。
// presetBoard - 填滿格子
func (board *Board) presetBoard() bool {
row, col, foundEmpty := -1, -1, false
// 找到第一個非空的格子來填
for r := 0; r < BoardSize && !foundEmpty; r++ {
for c := 0; c < BoardSize && !foundEmpty; c++ {
if board.Cells[r][c].Value == 0 && board.Cells[r][c].Type != Preset {
row, col, foundEmpty = r, c, true
}
}
}
// 當所有都填滿了回傳 true
if !foundEmpty {
return true
}
// 隨機取值出來填寫
for _, digit := range digitsShuffled() {
// 確認 digit 是否可以填入 row, col
if board.isSafe(row, col, digit) {
// 先填入 row, col 為 digit
board.Cells[row][col].Type = Preset
board.Cells[row][col].Value = digit
// 如果格子填滿則回傳 true
if board.presetBoard() {
return true
}
// 否則把 row, col 回朔
board.Cells[row][col].Type = Empty
board.Cells[row][col].Value = 0
}
}
return false
}
// GenerateSolution - 產生解法
func (board *Board) GenerateSolution() {
// 填入解法
board.presetBoard()
}
// MakePuzzleFromSolution - 建立題目
func (board *Board) MakePuzzleFromSolution(targetClues int) {
puzzle := board.Clone()
order := coordsShuffled()
for _, rc := range order {
if puzzle.presetedCount() <= targetClues {
break
}
r, c := rc[0], rc[1]
if puzzle.Cells[r][c].Type == Empty {
continue
}
tmp := puzzle.Cells[r][c]
puzzle.Cells[r][c].Type = Empty
puzzle.Cells[r][c].Value = 0
if puzzle.hasUniqueSolution() {
// 不是唯一解 → 復原
puzzle.Cells[r][c].Type = tmp.Type
puzzle.Cells[r][c].Value = tmp.Value
}
}
board = &puzzle
}
func TestIsSafe(t *testing.T) {
type coord struct {
Row int
Col int
}
tests := []struct {
name string
targetCoord coord
targetValue int
setup func() *Board
want bool
}{
{
name: "check if board is safe to put 1, 2 with value 9, should be false, for setup board.Cells[1][1].Value=9",
targetCoord: coord{
Row: 1,
Col: 2,
},
targetValue: 9,
setup: func() *Board {
board := NewBoard()
board.Cells[1][1].Type = Preset
board.Cells[1][1].Value = 9
return board
},
want: false,
},
{
name: "check if board is safe to put 2, 5 with value 9, should be true, for setup board.Cells[1][1].Value=9, boards.Cells[4][3].Value = 9",
targetCoord: coord{
Row: 2,
Col: 5,
},
targetValue: 9,
setup: func() *Board {
board := NewBoard()
board.Cells[1][1].Type = Preset
board.Cells[1][1].Value = 9
board.Cells[4][3].Type = Preset
board.Cells[4][3].Value = 9
return board
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
board := tt.setup()
got := board.isSafe(tt.targetCoord.Row, tt.targetCoord.Col, tt.targetValue)
assert.Equal(t, tt.want, got)
})
}
}
func TestMakePuzzleFromSolution(t *testing.T) {
tests := []struct {
name string
targetClues int
wantCluesCount int
setup func() *Board
}{
{
name: "generate puzzle with Easy Level",
targetClues: int(Easy),
wantCluesCount: int(Easy),
setup: func() *Board {
board := NewBoard()
board.GenerateSolution()
return board
},
},
{
name: "generate puzzle with Medium Level",
targetClues: int(Medium),
wantCluesCount: int(Medium),
setup: func() *Board {
board := NewBoard()
board.GenerateSolution()
return board
},
},
{
name: "generate puzzle with Hard Level",
targetClues: int(Hard),
wantCluesCount: int(Hard),
setup: func() *Board {
board := NewBoard()
board.GenerateSolution()
return board
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
board := tt.setup()
board.MakePuzzleFromSolution(tt.targetClues)
got := board.presetedCount()
assert.Equal(t, tt.wantCluesCount, got)
})
}
}
https://github.com/leetcode-golang-classroom/sudoku-game/actions/runs/17518677864/job/49759560227
在前幾篇文章中,我們已經完成了 Sudoku 的盤面資料結構、題目載入、合法性檢查,以及利用 Backtracking 生成題目。這些都屬於「資料面」的準備。
接下來,我們將邁向遊戲的「畫面呈現」,也就是使用 Ebiten 來繪製 Sudoku 的 9x9 棋盤。