Game Development 21 min read

Build a Classic Minesweeper Game in Go with Ebiten – Full Source Code Explained

This article provides a complete Go implementation of the classic Minesweeper game using the Ebiten library, including source code, detailed explanations of game mechanics, instructions for compiling and running both desktop and browser versions, and screenshots demonstrating the gameplay.

Open Source Linux
Open Source Linux
Open Source Linux
Build a Classic Minesweeper Game in Go with Ebiten – Full Source Code Explained

This guide walks you through creating a fully functional Minesweeper clone in Go using the Ebiten game engine. It covers the data structures, game logic, rendering, input handling, and how to load resources such as tiles and faces.

package main

import (
    "archive/zip"
    "bytes"
    "encoding/base64"
    "fmt"
    "image"
    "image/color"
    "image/png"
    "log"
    "math/rand"
    "strings"
    "time"

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
    "github.com/hajimehoshi/ebiten/v2/inpututil"
)

func main() {
    //goland:noinspection GoDeprecation
    rand.Seed(time.Now().Unix())
    m := &mine{h: 16, w: 30, mineCnt: 99}
    err := m.loadResources()
    if err != nil {
        log.Fatal(err)
    }
    m.initData() // 开局初始数据
    ebiten.SetWindowTitle("Mine Sweeping")
    if err = ebiten.RunGame(m); err != nil {
        log.Fatal(err)
    }
}

const (
    gridHW = 16 // 格子宽高
)

type (
    mine struct {
        h, w int
        playing int // 0: 正常,1: 赢,2: 输
        data [][]*grid
        mineCnt int
        timeStart time.Time
        timeCnt int
        timeX float64
        gridW, gridH int
        faceNum int
        faceX float64
        isFace func(h, w int) bool
        img, num, face []*ebiten.Image
        background *ebiten.Image
        text string
    }

    grid struct {
        data int // 格子数据
        state int // 状态
    }
)

var aroundPos = [][]int{{-1, 1, 0, 0, -1, -1, 1, 1}, {0, 0, -1, 1, -1, 1, -1, 1}}

func (m *mine) around(h, w int, f func(h, w int)) {
    var i, nh, nw int
    for ; i < 8; i++ {
        nh, nw = h+aroundPos[0][i], w+aroundPos[1][i]
        if nh >= 0 && nh < m.h && nw >= 0 && nw < m.w {
            f(nh, nw) // [h,w]周围合法8个位置
        }
    }
}

func (m *mine) initData() {
    var i, j int
    if len(m.data) < m.h {
        m.data = make([][]*grid, m.h)
    }
    for i = 0; i < m.h; i++ {
        if len(m.data[i]) < m.w {
            m.data[i] = make([]*grid, m.w)
        }
    }
    for i = 0; i < m.h; i++ {
        for j = 0; j < m.w; j++ {
            if d := m.data[i][j]; d == nil {
                m.data[i][j] = new(grid)
            } else {
                d.data, d.state = 0, 0
            }
        }
    }
    cnt := 0
    for cnt < m.mineCnt {
        i, j = rand.Intn(m.h), rand.Intn(m.w)
        if d := m.data[i][j]; d.data != 10 {
            cnt++
            d.data = 10
        }
    }
    for i = 0; i < m.h; i++ {
        for j = 0; j < m.w; j++ {
            if d := m.data[i][j]; d.data != 10 {
                m.around(i, j, func(h, w int) {
                    if m.data[h][w].data == 10 {
                        d.data++
                    }
                })
            }
        }
    }
    m.playing = 0
    m.timeStart = time.Time{}
    m.timeCnt = 0
    m.gridW, m.gridH = m.w*gridHW+6, (m.h+3)*gridHW+6
    faceX := m.gridW/2 - 18
    m.faceX = float64(faceX)
    m.isFace = func(h, w int) bool { return h >= 4 && h <= 28 && w >= faceX && w < faceX+24 }
    m.faceNum = 0
    m.timeX = float64(m.gridW - 18)
    ebiten.SetWindowSize(m.gridW, m.gridH)
    m.background = ebiten.NewImage(m.gridW, m.gridH-gridHW)
    m.background.Fill(backgroundColor) // 创建背景图片
    m.text = fmt.Sprintf("H:%d,W:%d,M:%d >", m.h, m.w, m.mineCnt)
}

func (m *mine) cursorPos() (h, w, state int) {
    w, h = ebiten.CursorPosition()
    if m.isFace(h, w) {
        state = 1
    } else {
        w, h = (w-3)/gridHW, h/gridHW-2
        if w >= 0 && w < m.w && h >= 0 && h < m.h {
            state = 2
        }
    }
    return
}

func (m *mine) reactionChain(h, w int) {
    d := m.data[h][w]
    if d.state != 0 {
        return // 已打开或插旗 或 游戏完成
    }
    d.state = -1
    switch d.data {
    case 10:
        if m.playing == 0 {
            m.playing = 2
            d.data = 12 // 游戏结束,标记第1个踩到的雷
        }
    case 0: // 递归点开所有空白区域
        m.around(h, w, func(h, w int) { m.reactionChain(h, w) })
    }
}

func (m *mine) Update() error {
    var state int
    if m.playing != 0 {
        if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
            _, _, state = m.cursorPos()
            if state == 1 {
                m.faceNum = 4
            } else {
                switch m.playing {
                case 1:
                    m.faceNum = 3
                case 2:
                    m.faceNum = 2
                }
            }
        } else if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
            _, _, state = m.cursorPos()
            if state == 1 {
                m.initData() // 左键小脸松开重新开始游戏
            }
        }
        return nil
    }
    if m.timeCnt < 999 && !m.timeStart.IsZero() {
        m.timeCnt = int(time.Since(m.timeStart) / time.Second)
    }
    var d *grid
    var i, j int
    if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
        for i = 0; i < m.h; i++ {
            for j = 0; j < m.w; j++ {
                if d = m.data[i][j]; d.state == 1 {
                    d.state = 0
                }
            }
        }
        i, j, state = m.cursorPos()
        switch state {
        case 1:
            m.faceNum = 4
        case 2:
            switch d = m.data[i][j]; d.state {
            case 0:
                d.state = 1
            case -1:
                m.around(i, j, func(ah, aw int) {
                    if ad := m.data[ah][aw]; ad.state == 0 {
                        ad.state = 1
                    }
                })
            }
            m.faceNum = 1
        default:
            m.faceNum = 1
        }
    } else if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
        for i = 0; i < m.h; i++ {
            for j = 0; j < m.w; j++ {
                if d = m.data[i][j]; d.state == 1 {
                    d.state = 0
                }
            }
        }
        m.faceNum = 0
        i, j, state = m.cursorPos()
        switch state {
        case 1:
            m.initData() // 笑脸位置松开左键,重新开局
            return nil
        case 2:
            if m.timeStart.IsZero() {
                m.timeStart = time.Now()
            }
            switch d = m.data[i][j]; d.state {
            case 0: // 判断单击
                m.reactionChain(i, j)
            case -1: // 判断双击
                if d.data >= 1 && d.data <= 8 {
                    state = 0
                    m.around(i, j, func(ah, aw int) {
                        if ad := m.data[ah][aw]; ad.state == 2 {
                            state++
                        }
                    })
                    if d.data == state {
                        m.around(i, j, func(ah, aw int) { m.reactionChain(ah, aw) })
                    }
                }
            }
            if m.playing == 2 {
                m.faceNum = 2 // 游戏结束,输了
                for i = 0; i < m.h; i++ {
                    for j = 0; j < m.w; j++ {
                        switch d = m.data[i][j]; d.state {
                        case 0: // 将所有雷打开
                            if d.data == 10 {
                                d.state = -1
                            }
                        case 2: // 插旗位置不是雷,设置标雷错误
                            if d.data != 10 {
                                d.state = -1
                                d.data = 11
                            }
                        }
                    }
                }
                return nil
            }
            state = 0
            for i = 0; i < m.h; i++ {
                for j = 0; j < m.w; j++ {
                    if d = m.data[i][j]; d.state == -1 {
                        state++
                    }
                }
            }
            // 点开位置 + 总雷数 = 全部格子数, 此时赢
            if state+m.mineCnt == m.h*m.w {
                m.faceNum = 3 // 游戏结束,赢了
                m.playing = 1
                for i = 0; i < m.h; i++ {
                    for j = 0; j < m.w; j++ {
                        if d = m.data[i][j]; d.state == 0 {
                            d.state = 2 // 剩余全插旗
                        }
                    }
                }
                return nil
            }
        }
    } else if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) {
        i, j, state = m.cursorPos()
        if state == 2 {
            switch d = m.data[i][j]; d.state {
            case 0:
                d.state = 2 // 插旗
            case 2:
                d.state = 0 // 取消
            }
        }
    }
    for k, v := range eKey {
        if inpututil.IsKeyJustReleased(k) {
            switch v {
            case "d":
                if i = len(m.text) - 1; m.text[i] != '>' {
                    m.text = m.text[:i]
                }
            case "e":
                i = strings.IndexByte(m.text, '>') + 1
                var ok bool
                n, _ := fmt.Sscanf(m.text[i:], "%d %d %d", &i, &j, &state)
                switch n {
                case 3: // 读取 h/w/mine 这3个数据
                    if i >= 9 && i <= 45 && j >= 9 && j <= 45 && state >= 10 && state <= (i-1)*(j-1) {
                        m.h, m.w, m.mineCnt = i, j, state
                        ok = true
                    }
                case 1: // 输入单个数字切换难度模式
                    switch i {
                    case 1:
                        m.h, m.w, m.mineCnt = 9, 9, 10 // 初级
                        ok = true
                    case 2:
                        m.h, m.w, m.mineCnt = 16, 16, 40 // 中级
                        ok = true
                    case 3:
                        m.h, m.w, m.mineCnt = 16, 30, 99 // 高级
                        ok = true
                    case 4:
                        m.h, m.w, m.mineCnt = 24, 30, 99 // 最大
                        ok = true
                    }
                }
                if ok {
                    m.initData()
                    return nil
                }
            default:
                m.text += v
            }
        }
    }
    return nil
}

var (
    eKey = map[ebiten.Key]string{
        ebiten.KeyDigit0: "0",
        ebiten.KeyDigit1: "1",
        ebiten.KeyDigit2: "2",
        ebiten.KeyDigit3: "3",
        ebiten.KeyDigit4: "4",
        ebiten.KeyDigit5: "5",
        ebiten.KeyDigit6: "6",
        ebiten.KeyDigit7: "7",
        ebiten.KeyDigit8: "8",
        ebiten.KeyDigit9: "9",
        ebiten.KeySpace:  " ",
        ebiten.KeyBackspace:   "d", // 删除
        ebiten.KeyEnter:       "e", // 回车
        ebiten.KeyNumpadEnter: "e", // 回车
    }
    backgroundColor = color.RGBA{R: 0xc0, G: 0xc0, B: 0xc0, A: 0xff}
)

func (m *mine) Draw(screen *ebiten.Image) {
    screen.DrawImage(m.background, nil)
    ct := m.mineCnt
    op := &ebiten.DrawImageOptions{}
    op.GeoM.Translate(3, 2*gridHW)
    for i := 0; i < m.h; i++ {
        for j := 0; j < m.w; j++ {
            switch d := m.data[i][j]; d.state {
            case 0: // 默认状态
                screen.DrawImage(m.img[15], op)
            case 1: // 按住左键不松开
                screen.DrawImage(m.img[0], op)
            case 2: // 标记旗子
                screen.DrawImage(m.img[14], op)
                ct--
            default: // 按照数据显示
                if d.data == 11 {
                    ct-- // 错误插旗也算标雷
                }
                screen.DrawImage(m.img[d.data], op)
            }
            op.GeoM.Translate(gridHW, 0)
        }
        op.GeoM.Translate(0, gridHW)
        op.GeoM.SetElement(0, 2, 3)
    }
    op.GeoM.Reset()
    op.GeoM.Translate(5, 5)
    var num []int
    if ct >= 0 {
        num = []int{(ct / 100) % 10, (ct / 10) % 10, ct % 10}
    } else {
        ct = -ct // 负数只显示2位
        num = []int{11, (ct / 10) % 10, ct % 10}
    }
    for _, v := range num {
        screen.DrawImage(m.num[v], op)
        op.GeoM.Translate(13, 0)
    }
    op.GeoM.Reset()
    op.GeoM.Translate(m.timeX, 5)
    for _, v := range []int{m.timeCnt % 10, (m.timeCnt / 10) % 10, (m.timeCnt / 100) % 10} {
        screen.DrawImage(m.num[v], op)
        op.GeoM.Translate(-13, 0)
    }
    op.GeoM.Reset()
    op.GeoM.Translate(m.faceX, 4)
    screen.DrawImage(m.face[m.faceNum], op)
    ebitenutil.DebugPrintAt(screen, m.text, 10, m.gridH-gridHW)
}

func (m *mine) Layout(_, _ int) (int, int) { return m.gridW, m.gridH }

func (m *mine) loadResources() error {
    const sd = "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"
    bd, err := base64.StdEncoding.DecodeString(sd)
    if err != nil {
        return err
    }
    data := bytes.NewReader(bd)
    zr, err := zip.NewReader(data, data.Size())
    if err != nil {
        return err
    }
    subImg := func(img image.Image, x, y, num int) []*ebiten.Image {
        ei := ebiten.NewImageFromImage(img)
        ri := make([]*ebiten.Image, num)
        for i := 0; i < num; i++ {
            var rc image.Rectangle
            rc.Min.Y, rc.Max.X = i*y, x
            rc.Max.Y = rc.Min.Y + y
            ri[num-i-1] = ei.SubImage(rc).(*ebiten.Image)
        }
        return ri
    }
    for _, fv := range zr.File {
        fr, err := fv.Open()
        if err != nil {
            return err
        }
        img, err := png.Decode(fr)
        _ = fr.Close()
        if err != nil {
            return err
        }
        switch fv.Name {
        case "ico.png":
            ebiten.SetWindowIcon([]image.Image{img})
        case "mine.png":
            m.img = subImg(img, 16, 16, 16)
        case "num.png":
            m.num = subImg(img, 13, 23, 12)
        case "face.png":
            m.face = subImg(img, 24, 24, 5)
        }
    }
    return nil
}

To run the game on a desktop, compile the project with ./build.bat minesweeper and execute the resulting binary. For a web version, start the provided HTTP server ( ./httpServer) and open http://127.0.0.1:8080 in a browser.

Minesweeper screenshot
Minesweeper screenshot
Minesweeper browser screenshot
Minesweeper browser screenshot
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

WebGoGame DevelopmentDesktopsource codeMinesweeperEbiten
Open Source Linux
Written by

Open Source Linux

Focused on sharing Linux/Unix content, covering fundamentals, system development, network programming, automation/operations, cloud computing, and related professional knowledge.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.