SOURCE

console 命令行工具 X clear

                    
>
console
(function (Sweep) {
    const W = 20, H = 20, SIZE = 20, difficulty = .1
    const WIDTH = W * SIZE, HEIGHT = H * SIZE
    const { points, openPoint } = new Sweep(W, H, difficulty)
    const canvas = document.createElement("canvas")
    canvas.width = WIDTH
    canvas.height = HEIGHT
    const ctx = canvas.getContext("2d")
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'

    const render = function () {
        ctx.clearRect(0, 0, WIDTH, HEIGHT)
        for (let x = 0; x < W; x++) {
            let line = points[x]
            for (let y = 0; y < H; y++) {
                const { n, isMine, isOpen, flag } = line[y]
                if (flag) {
                    ctx.fillStyle = 'green'
                    ctx.fillText('✅', (x + .5) * SIZE, (y + .5) * SIZE)
                } else if (isOpen) {
                    ctx.fillStyle = '#fff'
                    ctx.fillRect(x * W, y * H, SIZE, SIZE)
                    if (n > 0) {
                        ctx.fillStyle = ['#fff', '#333', 'green', 'yellow', 'orange', '#c00', '#f00', '#f00'][n]
                        ctx.fillText(n, (x + .5) * SIZE, (y + .5) * SIZE)
                    }
                    if (isMine) {
                        ctx.fillStyle = 'red'
                        ctx.fillText('雷', (x + .5) * SIZE, (y + .5) * SIZE)
                    }
                } else {
                    ctx.fillStyle = '#d2d2d2'
                    ctx.fillRect(x * W, y * H, SIZE, SIZE)
                }
                ctx.strokeStyle = '#ccc'
                ctx.strokeRect(x * W, y * H, SIZE, SIZE)
            }
        }
    }
    let clicking
    canvas.addEventListener('click', function (e) {
        const { offsetX, offsetY } = e
        openPoint(offsetX / SIZE | 0, offsetY / SIZE | 0)
        render();
        // 原生不支持,模拟 双击事件
        if (clicking) {
            const eve = new CustomEvent('dblclick')
            Object.assign(eve, { offsetX, offsetY })
            canvas.dispatchEvent(eve)
        }
        clicking = true
        setTimeout(function() {
            clicking = false
        }, 300);
    })
    canvas.addEventListener('dblclick', function (e) {
        const { offsetX, offsetY } = e
        const x = offsetX / SIZE | 0
        const y = offsetY / SIZE | 0
        const a = points;
        // 找到周围的点把没有插旗的都打开
        [
            a[x - 1] && a[x - 1][y - 1],
            a[x - 1] && a[x - 1][y],
            a[x - 1] && a[x - 1][y + 1],
            a[x][y - 1],
            a[x][y + 1],
            a[x + 1] && a[x + 1][y - 1],
            a[x + 1] && a[x + 1][y],
            a[x + 1] && a[x + 1][y + 1]
        ].map(p => {
            if (p && !p.flag) {
                openPoint(p.x, p.y)
            }
        })
        render()
    })
    // 插旗
    canvas.addEventListener('contextmenu', function (e) {
        e.preventDefault()
        const { offsetX, offsetY } = e
        const point = points[offsetX / SIZE | 0][offsetY / SIZE | 0]
        point.flag = !point.flag
        render();
    })
    document.body.appendChild(canvas)
    render();
})(function () {
    class Point {
        constructor(x, y, isMine) {
            this.x = x
            this.y = y
            this.isMine = !!isMine
            this.n = 0
            this.isOpen = false
            this.flag = false
        }
    }
    class Sweep {
        constructor(W, H, difficulty) {
            this.W = W
            this.H = H
            this.dt = difficulty || .1
            this.openPoint = this.openPoint.bind(this)
            this.getNum = this.getNum.bind(this)
            this._init()
        }
        // 点开, 需要看看上下左右紧挨着的有没有数字,没有的话,继续打开
        openPoint(x, y) {
            const { points, pointList, openPoint } = this
            const a = points
            const p = a[x] && a[x][y]
            this.beginTime = this.beginTime || Date.now()
            if (p && !p.isOpen) {
                p.isOpen = true
                if (p.isMine) {
                    setTimeout(function() {
                        alert('game over!')
                        window.location.reload()
                    }, 0);
                } else if (!pointList.find((pt) => pt.isMine === pt.isOpen)) {
                    alert(`success! \n time: ${(Date.now() - this.beginTime) / 1000 | 0}s`)
                } else if (!p.isMine && !p.n) {
                    openPoint(x - 1, y)
                    openPoint(x + 1, y)
                    openPoint(x, y - 1)
                    openPoint(x, y + 1)
                }
            }
        }
        // 初始化 雷 完成以后,算一下每个点周围的雷的数量,打标
        getNum(p, a) {
            const { x, y } = p
            if (!p.isMine) {
                p.n = [
                    a[x - 1] && a[x - 1][y - 1],
                    a[x - 1] && a[x - 1][y],
                    a[x - 1] && a[x - 1][y + 1],
                    a[x][y - 1],
                    a[x][y + 1],
                    a[x + 1] && a[x + 1][y - 1],
                    a[x + 1] && a[x + 1][y],
                    a[x + 1] && a[x + 1][y + 1]
                ].filter(m => m && m.isMine).length
            }
        }
        // 初始化 W * H 个点 并计算数字
        _init() {
            const { W, H, dt, getNum } = this
            let points = []
            // 循环随机生成雷
            for (let x = 0; x < W; x++) {
                let line = []
                for (let y = 0; y < H; y++) {
                    line.push(new Point(x, y, Math.random() < dt))
                }
                points.push(line)
            }
            // 已经有雷,循环计算
            for (let x = 0; x < W; x++) {
                let line = points[x]
                for (let y = 0; y < H; y++) {
                    getNum(line[y], points)
                }
            }
            this.pointList = points.reduce((a, line) => a.concat(line), [])
            this.points = points
        }
    }
    return Sweep
}())
<html>
    <body>
	
</body>
</html>