Trae CN solo模式初体验
今天体验了字节Trae CN个人版的Solo开发模式,号称0-1的动作。我以windows11下常见的扫雷程序为例测试了一把,实现一版简易扫雷,第一次使用体验,全程4小时,修改多次。开发语言是Golang。Windows11环境。Go开发环境让Trae 去给安装建议步骤,这一点比较省事。// 直接运行图形界面版本。
·
背景
今天体验了字节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')
}
更多推荐



所有评论(0)