背景

今天体验了字节Trae CN个人版的Solo开发模式,号称0-1的动作。我以windows11下常见的扫雷程序为例测试了一把,实现一版简易扫雷,第一次使用体验,全程4小时,修改多次。
开发语言是Golang。Windows11环境。Go开发环境让Trae 去给安装建议步骤,这一点比较省事。

结论

  • 简化版扫雷做出来了。不说过程,结果是做出来了。是0-1过程。
  • windows扫雷程序,提取需求文字,纯粹是根据个人玩的总结。一开始还是让trae给了一版需求,然后不断迭代改bug。Trae的初始需求版本太简单,如果能一开始给出最终版需求那开发速度就快多了。
  • 没有skill,没有memory, Solo还是记不住需求,综合考虑。只能每次喂需求,比较机械,不能保存记忆,多条件综合考虑,导致bug比较多。
  • 默认使用的模型,经常需要等待,体验感不是很好。
  • 真实的扫雷程序前端界面的需求相对还是比较复杂,Solo还有待检验。生产环境单用个人版还是不够,还需要结合skill, MCP,Agent等一起用。当然个人版仅仅是体验。
  • 需求的整理非常重要,越详细越好。整理好需求是第一重要。

过程

代码如下:

$ tree -L 1
.saolei
|-- debug.log
|-- game.go
|-- go.mod
|-- go.sum
|-- gui.go
|-- main.go
|-- saolei.exe
`-- ui.go

编译指令(CMD):

go build -ldflags -H=windowsgui -o saolei.exe main.go game.go ui.go gui.go

实现的界面如下:
在这里插入图片描述
相关文件内容如下:

// game.go
package main

import (
	"math/rand"
	"time"
)

// 游戏难度设置
type Difficulty struct {
	Name      string
	Rows      int
	Cols      int
	MineCount int
}

// 预定义难度
var (
	Easy   = Difficulty{"Easy", 9, 9, 10}
	Medium = Difficulty{"Medium", 16, 16, 40}
	Hard   = Difficulty{"Hard", 16, 30, 99}
)

// 格子状态
type Cell struct {
	IsMine        bool
	IsRevealed    bool
	IsFlagged     bool
	NeighborMines int
}

// 游戏状态
type GameState string

const (
	GameStateReady   GameState = "ready"
	GameStatePlaying GameState = "playing"
	GameStateWon     GameState = "won"
	GameStateLost    GameState = "lost"
	GameStatePaused  GameState = "paused"
)

// 游戏结构体
type Game struct {
	Board       [][]Cell
	Difficulty  Difficulty
	State       GameState
	MineCount   int
	FlagsLeft   int
	StartTime   time.Time
	ElapsedTime time.Duration
	History     []GameRecord
}

// 游戏记录
type GameRecord struct {
	Difficulty string
	Time       time.Duration
	Date       time.Time
}

// 创建新游戏
func NewGame() *Game {
	// 初始化随机数生成器
	rand.Seed(time.Now().UnixNano())

	// 默认使用初级难度
	game := &Game{
		Difficulty: Easy,
		State:      GameStateReady,
		MineCount:  Easy.MineCount,
		FlagsLeft:  Easy.MineCount,
	}

	// 初始化棋盘
	game.InitBoard()

	return game
}

// 初始化棋盘
func (g *Game) InitBoard() {
	rows := g.Difficulty.Rows
	cols := g.Difficulty.Cols

	// 创建棋盘
	g.Board = make([][]Cell, rows)
	for i := range g.Board {
		g.Board[i] = make([]Cell, cols)
	}
}

// 生成地雷
func (g *Game) GenerateMines(firstRow, firstCol int) {
	rows := g.Difficulty.Rows
	cols := g.Difficulty.Cols
	mineCount := g.Difficulty.MineCount

	// 确保第一次点击的位置不是地雷
	excluded := make(map[int]bool)
	excluded[firstRow*cols+firstCol] = true

	// 生成地雷
	placed := 0
	for placed < mineCount {
		row := rand.Intn(rows)
		col := rand.Intn(cols)
		pos := row*cols + col

		// 跳过已放置地雷的位置和第一次点击的位置
		if !excluded[pos] && !g.Board[row][col].IsMine {
			g.Board[row][col].IsMine = true
			excluded[pos] = true
			placed++
		}
	}

	// 计算每个格子周围的地雷数
	g.CalculateNeighborMines()
}

// 计算每个格子周围的地雷数
func (g *Game) CalculateNeighborMines() {
	rows := g.Difficulty.Rows
	cols := g.Difficulty.Cols

	for i := 0; i < rows; i++ {
		for j := 0; j < cols; j++ {
			if !g.Board[i][j].IsMine {
				count := 0
				// 检查周围8个方向
				for di := -1; di <= 1; di++ {
					for dj := -1; dj <= 1; dj++ {
						ni, nj := i+di, j+dj
						if ni >= 0 && ni < rows && nj >= 0 && nj < cols && g.Board[ni][nj].IsMine {
							count++
						}
					}
				}
				g.Board[i][j].NeighborMines = count
			}
		}
	}
}

// 翻开格子
func (g *Game) RevealCell(row, col int) bool {
	// 检查坐标是否有效
	if row < 0 || row >= g.Difficulty.Rows || col < 0 || col >= g.Difficulty.Cols {
		return false
	}

	cell := &g.Board[row][col]

	// 如果格子已经翻开或标记,直接返回
	if cell.IsRevealed || cell.IsFlagged {
		return false
	}

	// 如果是第一次点击,生成地雷
	if g.State == GameStateReady {
		g.State = GameStatePlaying
		g.StartTime = time.Now()
		g.GenerateMines(row, col)
	}

	// 翻开格子
	cell.IsRevealed = true

	// 如果是地雷,游戏结束
	if cell.IsMine {
		g.State = GameStateLost
		return false
	}

	// 如果是空白格子,递归翻开周围的格子
	if cell.NeighborMines == 0 {
		for di := -1; di <= 1; di++ {
			for dj := -1; dj <= 1; dj++ {
				if di != 0 || dj != 0 {
					g.RevealCell(row+di, col+dj)
				}
			}
		}
	}

	// 检查是否获胜
	g.CheckWin()
	return true
}

// 标记格子
func (g *Game) ToggleFlag(row, col int) bool {
	// 检查坐标是否有效
	if row < 0 || row >= g.Difficulty.Rows || col < 0 || col >= g.Difficulty.Cols {
		return false
	}

	cell := &g.Board[row][col]

	// 如果格子已经翻开,不能标记
	if cell.IsRevealed {
		return false
	}

	// 切换标记状态
	cell.IsFlagged = !cell.IsFlagged

	// 更新剩余标记数
	if cell.IsFlagged {
		g.FlagsLeft--
	} else {
		g.FlagsLeft++
	}

	// 检查是否获胜
	g.CheckWin()
	return true
}

// 检查是否获胜
func (g *Game) CheckWin() {
	rows := g.Difficulty.Rows
	cols := g.Difficulty.Cols

	// 检查是否所有非地雷格子都已翻开
	revealedCount := 0

	// 检查是否所有地雷都被标记,且标记的都是地雷
	flaggedMineCount := 0
	allFlagsAreMines := true

	for i := 0; i < rows; i++ {
		for j := 0; j < cols; j++ {
			if g.Board[i][j].IsRevealed && !g.Board[i][j].IsMine {
				revealedCount++
			}
			if g.Board[i][j].IsFlagged && g.Board[i][j].IsMine {
				flaggedMineCount++
			} else if g.Board[i][j].IsFlagged && !g.Board[i][j].IsMine {
				// 标记了非地雷格子,标记错误
				allFlagsAreMines = false
			}
		}
	}

	// 只有当所有地雷都被标记且标记的都是地雷时,游戏获胜
	if flaggedMineCount == g.MineCount && allFlagsAreMines {
		// 翻开所有非地雷格子
		for i := 0; i < rows; i++ {
			for j := 0; j < cols; j++ {
				if !g.Board[i][j].IsMine {
					g.Board[i][j].IsRevealed = true
				}
			}
		}

		g.State = GameStateWon
		g.ElapsedTime = time.Since(g.StartTime)
		// 记录游戏成绩
		g.AddRecord()
	}
}

// 游戏结束时翻开所有格子
func (g *Game) RevealAllCells() {
	rows := g.Difficulty.Rows
	cols := g.Difficulty.Cols

	for i := 0; i < rows; i++ {
		for j := 0; j < cols; j++ {
			g.Board[i][j].IsRevealed = true
		}
	}
}

// 翻开附近所有不是雷的#按键
func (g *Game) RevealNearbyCells(row, col int) {
	rows := g.Difficulty.Rows
	cols := g.Difficulty.Cols

	// 检查位置是否有效
	if row < 0 || row >= rows || col < 0 || col >= cols {
		return
	}

	// 检查该格子是否是雷
	cell := &g.Board[row][col]
	if cell.IsMine {
		return
	}

	// 如果格子未翻开,先翻开它
	if !cell.IsRevealed {
		g.RevealCell(row, col)
	}

	// 广度优先搜索,翻开所有与该格子相连的非雷#按键
	visited := make([][]bool, rows)
	for i := 0; i < rows; i++ {
		visited[i] = make([]bool, cols)
	}

	queue := make([][2]int, 0)
	queue = append(queue, [2]int{row, col})
	visited[row][col] = true

	for len(queue) > 0 {
		current := queue[0]
		queue = queue[1:]
		cr, cc := current[0], current[1]

		// 翻开周围8个格子
		for i := cr - 1; i <= cr+1; i++ {
			for j := cc - 1; j <= cc+1; j++ {
				if i >= 0 && i < rows && j >= 0 && j < cols && !visited[i][j] {
					neighborCell := &g.Board[i][j]
					if !neighborCell.IsMine && !neighborCell.IsRevealed && !neighborCell.IsFlagged {
						// 翻开格子
						g.RevealCell(i, j)
						// 如果是空白格子(周围没有地雷),则继续搜索
						if neighborCell.NeighborMines == 0 {
							queue = append(queue, [2]int{i, j})
						}
						visited[i][j] = true
					}
				}
			}
		}
	}

	// 检查是否获胜
	g.CheckWin()
}

// 添加游戏记录
func (g *Game) AddRecord() {
	record := GameRecord{
		Difficulty: g.Difficulty.Name,
		Time:       g.ElapsedTime,
		Date:       time.Now(),
	}
	g.History = append(g.History, record)
}

// 获取最短耗时
func (g *Game) GetBestTime() time.Duration {
	if len(g.History) == 0 {
		return 0
	}
	bestTime := g.History[0].Time
	for _, record := range g.History {
		if record.Difficulty == g.Difficulty.Name && record.Time < bestTime {
			bestTime = record.Time
		}
	}
	return bestTime
}

// 重新开始游戏
func (g *Game) Restart() {
	g.State = GameStateReady
	g.MineCount = g.Difficulty.MineCount
	g.FlagsLeft = g.Difficulty.MineCount
	g.InitBoard()
}

// 更改难度
func (g *Game) SetDifficulty(diff Difficulty) {
	g.Difficulty = diff
	g.Restart()
}

// 暂停游戏
func (g *Game) Pause() {
	if g.State == GameStatePlaying {
		g.State = GameStatePaused
		g.ElapsedTime = time.Since(g.StartTime)
	}
}

// 继续游戏
func (g *Game) Resume() {
	if g.State == GameStatePaused {
		g.State = GameStatePlaying
		g.StartTime = time.Now().Add(-g.ElapsedTime)
	}
}
// go.mod
module saolei

go 1.25.0

require fyne.io/fyne/v2 v2.7.3

require (
	fyne.io/systray v1.12.0 // indirect
	github.com/BurntSushi/toml v1.5.0 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/fredbi/uri v1.1.1 // indirect
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/fyne-io/gl-js v0.2.0 // indirect
	github.com/fyne-io/glfw-js v0.3.0 // indirect
	github.com/fyne-io/image v0.1.1 // indirect
	github.com/fyne-io/oksvg v0.2.0 // indirect
	github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
	github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
	github.com/go-text/render v0.2.0 // indirect
	github.com/go-text/typesetting v0.3.3 // indirect
	github.com/godbus/dbus/v5 v5.1.0 // indirect
	github.com/hack-pad/go-indexeddb v0.3.2 // indirect
	github.com/hack-pad/safejs v0.1.0 // indirect
	github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
	github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
	github.com/kr/text v0.2.0 // indirect
	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
	github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/rymdport/portal v0.4.2 // indirect
	github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
	github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
	github.com/stretchr/testify v1.11.1 // indirect
	github.com/yuin/goldmark v1.7.8 // indirect
	golang.org/x/image v0.24.0 // indirect
	golang.org/x/net v0.35.0 // indirect
	golang.org/x/sys v0.30.0 // indirect
	golang.org/x/text v0.22.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)
// go.sum
fyne.io/fyne/v2 v2.7.3 h1:xBT/iYbdnNHONWO38fZMBrVBiJG8rV/Jypmy4tVfRWE=
fyne.io/fyne/v2 v2.7.3/go.mod h1:gu+dlIcZWSzKZmnrY8Fbnj2Hirabv2ek+AKsfQ2bBlw=
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.3.3 h1:ihGNJU9KzdK2QRDy1Bm7FT5RFQoYb+3n3EIhI/4eaQc=
github.com/go-text/typesetting v0.3.3/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
```go

```go
// gui.go
package main

import (
	"fmt"
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

// 确保中文显示正常
func init() {
	// Fyne会自动使用系统中支持中文的字体
	// 如果需要,可以在这里设置自定义字体
}

// 全局变量,用于控制是否处于标记雷模式
var isFlagMode bool = false

// 全局变量,用于记录是否已经显示过成功提示窗口
var hasShownSuccessWindow bool = false

// 图形界面游戏
func RunGUI() {
	// 创建应用
	a := app.New()
	w := a.NewWindow("扫雷游戏")
	w.Resize(fyne.NewSize(800, 600))

	// 创建游戏实例
	game := NewGame()

	// 创建UI元素
	createUI(a, w, game)

	// 显示窗口
	w.ShowAndRun()
}

// 创建UI
func createUI(a fyne.App, w fyne.Window, game *Game) {
	// 顶部状态栏
	statusBar := createStatusBar(game)

	// 游戏棋盘
	board := createBoard(a, game, w)

	// 底部控制栏
	controlBar := createControlBar(a, game, w)

	// 操作说明
	instructions := widget.NewLabel("操作说明: 1. 单击:翻牌 2. 双击:翻开附近格子 3. 点击Flag按钮进入标记模式 4. 再次点击Flag按钮退出标记模式")

	// 组装布局
	content := container.NewVBox(
		statusBar,
		board,
		controlBar,
		instructions,
	)

	w.SetContent(content)

	// 定期更新UI
	go func() {
		for {
			time.Sleep(100 * time.Millisecond)
			updateUI(a, w, game)
		}
	}()
}

// 创建状态栏
func createStatusBar(game *Game) *widget.Label {
	bestTime := game.GetBestTime()
	bestTimeStr := "N/A"
	if bestTime > 0 {
		bestTimeStr = fmt.Sprintf("%.2f", bestTime.Seconds())
	}
	status := fmt.Sprintf("难度: %s | 地雷: %d | 标记: %d | 时间: 0.00s | 最短时间: %s",
		game.Difficulty.Name, game.MineCount, game.MineCount-game.FlagsLeft, bestTimeStr)
	return widget.NewLabel(status)
}

// 创建游戏棋盘
func createBoard(a fyne.App, game *Game, w fyne.Window) *fyne.Container {
	rows := game.Difficulty.Rows
	cols := game.Difficulty.Cols

	grid := container.NewGridWithColumns(cols)

	for i := 0; i < rows; i++ {
		for j := 0; j < cols; j++ {
			r, c := i, j
			// 创建一个容器来包装按钮,以便添加鼠标事件
			btn := widget.NewButton("#", nil)

			// 注意:Fyne的Button组件不直接支持双击事件和右键菜单
			// 我们可以通过以下方式实现:

			// 1. 记录点击时间,用于检测双击
			var lastClickTime time.Time

			// 2. 设置按钮的点击事件
			btn.OnTapped = func() {
				// 捕获当前的r和c值
				rCurrent, cCurrent := r, c
				currentTime := time.Now()
				// 检查是否是双击(500毫秒内的第二次点击)
				if currentTime.Sub(lastClickTime) < 500*time.Millisecond {
					// 双击:翻开附近所有不是雷的#按键
					game.RevealNearbyCells(rCurrent, cCurrent)
				} else {
					// 单击:翻牌
					game.RevealCell(rCurrent, cCurrent)
				}
				lastClickTime = currentTime
				updateUI(a, w, game)
			}

			// 3. 为了实现右键菜单,我们可以使用Canvas的AddShortcut方法
			// 但由于时间限制,我们暂时简化实现,使用一个额外的按钮来标记雷
			// 注意:在实际应用中,我们可以使用更复杂的方法来实现右键菜单
			btn.Resize(fyne.NewSize(30, 30))
			grid.Add(btn)
		}
	}

	return grid
}

// 创建控制栏
func createControlBar(a fyne.App, game *Game, w fyne.Window) *fyne.Container {
	restartBtn := widget.NewButton("Restart", func() {
		game.Restart()
		// 重置成功提示窗口显示状态
		hasShownSuccessWindow = false
		updateUI(a, w, game)
	})

	difficultyBtn := widget.NewButton("Difficulty", func() {
		showDifficultyDialog(a, game, w)
	})

	historyBtn := widget.NewButton("History", func() {
		showHistoryDialog(a, game, w)
	})

	// 添加标记雷按钮
	flagBtn := widget.NewButton("Flag", nil)
	flagBtn.OnTapped = func() {
		fyne.Do(func() {
			isFlagMode = !isFlagMode
			if isFlagMode {
				flagBtn.SetText("Flag (On)")
			} else {
				flagBtn.SetText("Flag")
			}
		})
	}

	return container.NewHBox(
		restartBtn,
		difficultyBtn,
		historyBtn,
		flagBtn,
	)
}

// 全局变量,用于存储棋盘按钮
var boardButtons [][]fyne.CanvasObject

// 更新UI
func updateUI(a fyne.App, w fyne.Window, game *Game) {
	fyne.Do(func() {
		// 更新状态栏
		content := w.Content().(*fyne.Container)
		statusBar := content.Objects[0].(*widget.Label)

		var elapsed string
		if string(game.State) == "playing" {
			elapsed = fmt.Sprintf("%.2f", time.Since(game.StartTime).Seconds())
		} else {
			elapsed = fmt.Sprintf("%.2f", game.ElapsedTime.Seconds())
		}

		bestTime := game.GetBestTime()
		bestTimeStr := "N/A"
		if bestTime > 0 {
			bestTimeStr = fmt.Sprintf("%.2f", bestTime.Seconds())
		}

		status := fmt.Sprintf("难度: %s | 地雷: %d | 标记: %d | 时间: %ss | 最短时间: %s",
			game.Difficulty.Name, game.MineCount, game.MineCount-game.FlagsLeft, elapsed, bestTimeStr)
		statusBar.SetText(status)

		// 更新棋盘
		board := content.Objects[1].(*fyne.Container)
		rows := game.Difficulty.Rows
		cols := game.Difficulty.Cols

		// 检查是否需要重新创建棋盘
		if len(boardButtons) != rows || len(boardButtons[0]) != cols {
			// 清空棋盘
			board.RemoveAll()

			// 重新创建棋盘按钮
			boardButtons = make([][]fyne.CanvasObject, rows)
			for i := 0; i < rows; i++ {
				boardButtons[i] = make([]fyne.CanvasObject, cols)
				for j := 0; j < cols; j++ {
					r, c := i, j
					// 创建按钮
					btn := widget.NewButton("#", nil)

					// 记录点击时间,用于检测双击
					var lastClickTime time.Time

					// 设置按钮的点击事件
					btn.OnTapped = func() {
						// 捕获当前的r和c值
						rCurrent, cCurrent := r, c
						currentTime := time.Now()
						// 检查是否是双击(500毫秒内的第二次点击)
						if currentTime.Sub(lastClickTime) < 500*time.Millisecond {
							// 双击:翻开附近所有不是雷的#按键
							game.RevealNearbyCells(rCurrent, cCurrent)
						} else {
							// 单击:根据模式执行不同操作
							if isFlagMode {
								// 标记模式:标记或取消标记雷
								game.ToggleFlag(rCurrent, cCurrent)
							} else {
								// 普通模式:翻牌
								game.RevealCell(rCurrent, cCurrent)
							}
						}
						lastClickTime = currentTime
						updateUI(a, w, game)
					}
					btn.Resize(fyne.NewSize(30, 30))
					boardButtons[i][j] = btn
					board.Add(btn)
				}
			}
		} else {
			// 更新现有按钮的文本和状态
			for i := 0; i < rows; i++ {
				for j := 0; j < cols; j++ {
					cell := game.Board[i][j]
					var text string
					if cell.IsFlagged {
						text = "*" // 标记雷时显示*
					} else if !cell.IsRevealed {
						text = "#"
					} else if cell.IsMine {
						text = "*"
					} else if cell.NeighborMines > 0 {
						text = fmt.Sprintf("%d", cell.NeighborMines)
					} else {
						text = " "
					}
					// 更新按钮文本
					if btn, ok := boardButtons[i][j].(*widget.Button); ok {
						btn.SetText(text)
						// 设置按钮颜色
						if cell.IsFlagged {
							// 标记为雷时设置为红色,无论是否正确
							btn.Importance = widget.HighImportance
						} else if cell.IsMine && string(game.State) == "lost" {
							// 触雷时设置为红色
							btn.Importance = widget.HighImportance
						} else {
							// 其他情况设置为普通颜色
							btn.Importance = widget.MediumImportance
						}
					}
				}
			}
		}

		// 刷新UI
		w.Content().Refresh()

		// 游戏成功时显示提示
		if string(game.State) == "won" && !hasShownSuccessWindow {
			// 标记已经显示过成功提示窗口
			hasShownSuccessWindow = true

			// 创建成功提示窗口
			successWindow := a.NewWindow("游戏成功")
			successContent := container.NewVBox(
				widget.NewLabel("恭喜你成功了!"),
				widget.NewLabel(fmt.Sprintf("用时: %.2f秒", game.ElapsedTime.Seconds())),
				widget.NewButton("OK", func() {
					// 关闭窗口
					successWindow.Close()
				}),
			)
			successWindow.SetContent(successContent)
			successWindow.Resize(fyne.NewSize(200, 150))
			successWindow.Show()
		}
	})
}

// 显示难度选择对话框
func showDifficultyDialog(a fyne.App, game *Game, w fyne.Window) {
	var popup *widget.PopUp
	popup = widget.NewModalPopUp(
		container.NewVBox(
			widget.NewLabel("Select Difficulty"),
			widget.NewButton("Easy (9x9, 10 mines)", func() {
				game.SetDifficulty(Easy)
				updateUI(a, w, game)
				popup.Hide()
			}),
			widget.NewButton("Medium (16x16, 40 mines)", func() {
				game.SetDifficulty(Medium)
				updateUI(a, w, game)
				popup.Hide()
			}),
			widget.NewButton("Hard (16x30, 99 mines)", func() {
				game.SetDifficulty(Hard)
				updateUI(a, w, game)
				popup.Hide()
			}),
		),
		w.Canvas(),
	)
	popup.Show()
}

// 显示历史记录对话框
func showHistoryDialog(a fyne.App, game *Game, w fyne.Window) {
	var content *fyne.Container

	if len(game.History) == 0 {
		content = container.NewVBox(widget.NewLabel("No history records"))
	} else {
		items := make([]fyne.CanvasObject, len(game.History)+1)
		items[0] = widget.NewLabel("History Records")

		for i, record := range game.History {
			items[i+1] = widget.NewLabel(
				fmt.Sprintf("%s - %.2fs - %s",
					record.Difficulty, record.Time.Seconds(), record.Date.Format("2006-01-02 15:04:05")),
			)
		}

		content = container.NewVBox(items...)
	}

	popup := widget.NewModalPopUp(
		content,
		w.Canvas(),
	)
	popup.Show()
}
// main.go
package main

func main() {
	// 直接运行图形界面版本
	RunGUI()
}
// ui.go
package main

import (
	"fmt"
	"os"
	"bufio"
	"strings"
	"strconv"
	"time"
)

// 运行游戏主循环
func (g *Game) Run() {
	for {
		g.Render()
		
		// 根据游戏状态处理用户输入
		switch g.State {
		case GameStateReady, GameStatePlaying, GameStatePaused:
			g.HandleInput()
		case GameStateWon:
			fmt.Println("恭喜你获胜了!")
			fmt.Printf("用时: %.2f秒\n", g.ElapsedTime.Seconds())
			g.HandleGameOver()
		case GameStateLost:
			fmt.Println("很遗憾,你踩到地雷了!")
			g.RevealAllMines()
			g.Render()
			g.HandleGameOver()
		}
	}
}

// 渲染游戏界面
func (g *Game) Render() {
	// 清屏
	fmt.Print("\033[H\033[2J")
	
	// 显示游戏标题
	fmt.Println("=== 扫雷游戏 ===")
	fmt.Printf("难度: %s (%dx%d, %d雷)\n", g.Difficulty.Name, g.Difficulty.Rows, g.Difficulty.Cols, g.Difficulty.MineCount)
	
	// 显示游戏状态
	fmt.Printf("状态: %s\n", g.State)
	
	// 显示计时器
	var elapsed time.Duration
	if g.State == GameStatePlaying {
		elapsed = time.Since(g.StartTime)
	} else {
		elapsed = g.ElapsedTime
	}
	fmt.Printf("时间: %.2f秒\n", elapsed.Seconds())
	
	// 显示剩余地雷数和标记数
	fmt.Printf("地雷: %d, 标记: %d\n", g.MineCount, g.MineCount-g.FlagsLeft)
	
	// 显示棋盘
	g.RenderBoard()
	
	// 显示操作提示
	fmt.Println("操作说明:")
	fmt.Println("  r 行 c 列 - 翻开格子 (如: 1 2)")
	fmt.Println("  f 行 c 列 - 标记/取消标记地雷 (如: f 1 2)")
	fmt.Println("  p - 暂停/继续游戏")
	fmt.Println("  n - 重新开始游戏")
	fmt.Println("  d - 更改难度")
	fmt.Println("  h - 查看历史记录")
	fmt.Println("  q - 退出游戏")
}

// 渲染棋盘
func (g *Game) RenderBoard() {
	rows := g.Difficulty.Rows
	cols := g.Difficulty.Cols
	
	// 显示列号
	fmt.Print("   ")
	for j := 0; j < cols; j++ {
		fmt.Printf("%2d ", j+1)
	}
	fmt.Println()
	
	// 显示棋盘
	for i := 0; i < rows; i++ {
		// 显示行号
		fmt.Printf("%2d ", i+1)
		
		// 显示每行的格子
		for j := 0; j < cols; j++ {
			cell := g.Board[i][j]
			if cell.IsFlagged {
				fmt.Print(" F ")
			} else if !cell.IsRevealed {
				fmt.Print(" # ")
			} else if cell.IsMine {
				fmt.Print(" * ")
			} else if cell.NeighborMines > 0 {
				fmt.Printf(" %d ", cell.NeighborMines)
			} else {
				fmt.Print("   ")
			}
		}
		fmt.Println()
	}
}

// 处理用户输入
func (g *Game) HandleInput() {
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("请输入操作: ")
	input, _ := reader.ReadString('\n')
	input = strings.TrimSpace(input)
	
	if input == "" {
		return
	}
	
	parts := strings.Fields(input)
	
	switch parts[0] {
	case "q":
		fmt.Println("游戏结束!")
		os.Exit(0)
	case "n":
		g.Restart()
	case "p":
		if g.State == GameStatePlaying {
			g.Pause()
		} else if g.State == GameStatePaused {
			g.Resume()
		}
	case "d":
		g.ChangeDifficulty()
	case "h":
		g.ShowHistory()
	case "f":
		// 标记地雷
		if len(parts) >= 3 {
			row, err1 := strconv.Atoi(parts[1])
			col, err2 := strconv.Atoi(parts[2])
			if err1 == nil && err2 == nil {
				g.ToggleFlag(row-1, col-1)
			}
		}
	default:
		// 翻开格子
		if len(parts) >= 2 {
			row, err1 := strconv.Atoi(parts[0])
			col, err2 := strconv.Atoi(parts[1])
			if err1 == nil && err2 == nil {
				g.RevealCell(row-1, col-1)
			}
		}
	}
}

// 显示所有地雷(游戏结束时)
func (g *Game) RevealAllMines() {
	rows := g.Difficulty.Rows
	cols := g.Difficulty.Cols
	
	for i := 0; i < rows; i++ {
		for j := 0; j < cols; j++ {
			if g.Board[i][j].IsMine {
				g.Board[i][j].IsRevealed = true
			}
		}
	}
}

// 处理游戏结束后的操作
func (g *Game) HandleGameOver() {
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("按 'n' 重新开始,按 'd' 更改难度,按 'q' 退出: ")
	input, _ := reader.ReadString('\n')
	input = strings.TrimSpace(input)
	
	switch input {
	case "n":
		g.Restart()
	case "d":
		g.ChangeDifficulty()
	case "q":
		fmt.Println("游戏结束!")
		os.Exit(0)
	}
}

// 更改游戏难度
func (g *Game) ChangeDifficulty() {
	fmt.Println("选择难度:")
	fmt.Println("1. 初级 (9x9, 10雷)")
	fmt.Println("2. 中级 (16x16, 40雷)")
	fmt.Println("3. 高级 (16x30, 99雷)")
	
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("请输入选择 (1-3): ")
	input, _ := reader.ReadString('\n')
	input = strings.TrimSpace(input)
	
	switch input {
	case "1":
		g.SetDifficulty(Easy)
	case "2":
		g.SetDifficulty(Medium)
	case "3":
		g.SetDifficulty(Hard)
	}
}

// 显示历史记录
func (g *Game) ShowHistory() {
	fmt.Println("=== 历史记录 ===")
	
	if len(g.History) == 0 {
		fmt.Println("暂无记录")
	} else {
		for i, record := range g.History {
			fmt.Printf("%d. %s - %.2f秒 - %s\n", i+1, record.Difficulty, record.Time.Seconds(), record.Date.Format("2006-01-02 15:04:05"))
		}
	}
	
	fmt.Print("按 Enter 键返回...")
	reader := bufio.NewReader(os.Stdin)
	reader.ReadString('\n')
}
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐