為了讓你們進階Canvas,我花7小時寫了3個有趣的小遊戲!!!

Sunshine_Lin發表於2022-01-25

前言

大家好,我是林三心,相信大家看了我前一篇canvas入門文章為了讓她10分鐘入門canvas,我熬夜寫了3個小專案和這篇文章,對canvas已經有了入門級的瞭解。今天,我又用canvas寫了三個有趣的小遊戲,來哄你們開心,沒錯,我的心裡只有你們,沒有她。

image.png

截圖2021-07-25 上午12.15.24.png

現在是凌晨0點15分,我們們開搞??????????,一邊除錯一邊把這篇文章寫了!!!

貪吃蛇?

最終效果如下:
貪吃蛇.gif
實現步驟分為以下幾步:

  • 1、把蛇畫出來
  • 2、讓蛇動起來
  • 3、隨機投放食物
  • 4、蛇吃食物
  • 5、邊緣檢測與撞自己檢測

1. 把蛇畫出來

其實畫蛇很簡單,蛇就是由蛇頭和蛇身組成,而其實都可以用正方格來表示,蛇頭就是一個方格,而蛇身可以是很多個方格

畫方格可以用ctx.fillRect來畫,蛇頭使用head表示,而蛇身使用陣列body來表示
截圖2021-07-24 下午10.43.46.png

// html
<canvas id="canvas" width="800" height="800"></canvas>

// js


draw()

function draw() {
    const canvas = document.getElementById('canvas')

    const ctx = canvas.getContext('2d')

    // 小方格的建構函式
    function Rect(x, y, width, height, color) {
        this.x = x
        this.y = y
        this.width = width
        this.height = height
        this.color = color
    }

    Rect.prototype.draw = function () {
        ctx.beginPath()
        ctx.fillStyle = this.color
        ctx.fillRect(this.x, this.y, this.width, this.height)
        ctx.strokeRect(this.x, this.y, this.width, this.height)
    }

    // 蛇的建構函式
    function Snake(length = 0) {

        this.length = length
        // 蛇頭
        this.head = new Rect(canvas.width / 2, canvas.height / 2, 40, 40, 'red')

        // 蛇身
        this.body = []

        let x = this.head.x - 40
        let y = this.head.y

        for (let i = 0; i < this.length; i++) {
            const rect = new Rect(x, y, 40, 40, 'yellow')
            this.body.push(rect)
            x -= 40
        }
    }

    Snake.prototype.drawSnake = function () {
        // 繪製蛇頭
        this.head.draw()
        // 繪製蛇身
        for (let i = 0; i < this.body.length; i++) {
            this.body[i].draw()
        }
    }

    const snake = new Snake(3)
    snake.drawSnake()
}

2. 讓蛇動起來

蛇動起來有兩種情況:

  • 1、蛇一開始就會預設向右移動
  • 2、通過方向鍵控制,往不同方向移動
    這兩種情況每秒都是移動一個方格的位置

讓蛇動起來,其實原理很簡單,我就以蛇向右移動來舉例子吧:
截圖2021-07-24 下午10.57.06.png

  • 1、蛇頭先右移一個方格距離,蛇身不動
  • 2、蛇身首部加一個方格
  • 3、蛇身尾部的方格去除
  • 4、利用定時器,造成蛇不斷向右移動的視覺

      Snake.prototype.moveSnake = function () {
          // 將蛇頭上一次狀態,拼到蛇身首部
          const rect = new Rect(this.head.x, this.head.y, this.head.width, this.head.height, 'yellow')
          this.body.unshift(rect)
    
          this.body.pop()
    
          // 根據方向,控制蛇頭的座標
          switch (this.direction) {
              case 0:
                  this.head.x -= this.head.width
                  break
              case 1:
                  this.head.y -= this.head.height
                  break
              case 2:
                  this.head.x += this.head.width
                  break
              case 3:
                  this.head.y += this.head.height
                  break
          }
      }
    
      document.onkeydown = function (e) {
          // 鍵盤事件
          e = e || window.event
          // 左37  上38  右39  下40
          switch (e.keyCode) {
              case 37:
                  console.log(37)
                  // 三元表示式,防止右移動時按左,下面同理(貪吃蛇可不能直接掉頭)
                  snake.direction = snake.direction === 2 ? 2 : 0
                  snake.moveSnake()
                  break
              case 38:
                  console.log(38)
                  snake.direction = snake.direction === 3 ? 3 : 1
                  break
              case 39:
                  console.log(39)
                  snake.direction = snake.direction === 0 ? 0 : 2
                  break
              case 40:
                  console.log(40)
                  snake.direction = snake.direction === 1 ? 1 : 3
                  break
    
          }
      }
    
      const snake = new Snake(3)
      // 預設direction為2,也就是右
      snake.direction = 2
      snake.drawSnake()
    
      function animate() {
          // 先清空
          ctx.clearRect(0, 0, canvas.width, canvas.height)
          // 移動
          snake.moveSnake()
          // 再畫
          snake.drawSnake()
      }
    
      var timer = setInterval(() => {
          animate()
      }, 100)
    }

    實現效果如下:

蛇動起來.gif

3. 隨機投放食物

隨機投放食物,也就是在畫布中隨機畫一個方格,要注意以下兩點:

  • 1、座標要在畫布範圍內
  • 2、食物不能投到蛇身或者蛇頭上(這樣會把蛇砸暈的嘿嘿)

      function randomFood(snake) {
          let isInSnake = true
          let rect
          while (isInSnake) {
              const x = Math.round(Math.random() * (canvas.width - 40) / 40) * 40
              const y = Math.round(Math.random() * (canvas.height - 40) / 40) * 40
              console.log(x, y)
              // 保證是40的倍數啊
              rect = new Rect(x, y, 40, 40, 'blue')
              // 判斷食物是否與蛇頭蛇身重疊
              if ((snake.head.x === x && snake.head.y === y) || snake.body.find(item => item.x === x && item.y === y)) {
                  isInSnake = true
                  continue
              } else {
                  isInSnake = false
              }
          }
          return rect
      }
    
      const snake = new Snake(3)
      // 預設direction為2,也就是右
      snake.direction = 2
      snake.drawSnake()
      // 建立隨機食物例項
      var food = randomFood(snake)
      // 畫出食物
      food.draw()
    
      function animate() {
          // 先清空
          ctx.clearRect(0, 0, canvas.width, canvas.height)
          // 移動
          snake.moveSnake()
          // 再畫
          snake.drawSnake()
          food.draw()
      }

    效果如下,隨機食物畫出來了:
    截圖2021-07-24 下午11.17.03.png

4. 蛇吃食物

其實蛇吃食物,很簡單理解,也就是蛇頭移動到跟食物的座標重疊時,就算是吃到食物了,注意兩點:

  • 1、吃到食物後,蛇身要延長一個空格
  • 2、吃到食物後,隨機食物要變換位置
const canvas = document.getElementById('canvas')

const ctx = canvas.getContext('2d')

// 定義一個全域性的是否吃到食物的一個變數
let isEatFood = false
    

    Snake.prototype.moveSnake = function () {
        // 將蛇頭上一次狀態,拼到蛇身首部
        const rect = new Rect(this.head.x, this.head.y, this.head.width, this.head.height, 'yellow')
        this.body.unshift(rect)

        // 判斷蛇頭是否與食物重疊,重疊就是吃到了,沒重疊就是沒吃到
        isEatFood = food && this.head.x === food.x && this.head.y === food.y

        // 我們們上面在蛇身首部插入方格
        if (!isEatFood) {
            // 沒吃到就要去尾,相當於整條蛇沒變長
            this.body.pop()
        } else {
            // 吃到了就不去尾,相當於整條蛇延長一個方格

            // 並且吃到了,就要重新生成一個隨機食物
            food = randomFood(this)
            food.draw()
            isEatFood = false
        }

        // 根據方向,控制蛇頭的座標
        switch (this.direction) {
            case 0:
                this.head.x -= this.head.width
                break
            case 1:
                this.head.y -= this.head.height
                break
            case 2:
                this.head.x += this.head.width
                break
            case 3:
                this.head.y += this.head.height
                break
        }
    }

5. 碰邊界與碰自己

眾所周知,蛇頭碰到邊界,或者碰到蛇身,都會終止遊戲

    Snake.prototype.drawSnake = function () {
        // 如果碰到了
        if (isHit(this)) {
            // 清除定時器
            clearInterval(timer)
            const con = confirm(`總共吃了${this.body.length - this.length}個食物,重新開始嗎`)
            // 是否重開
            if (con) {
                draw()
            }
            return
        }
        // 繪製蛇頭
        this.head.draw()
        // 繪製蛇身
        for (let i = 0; i < this.body.length; i++) {
            this.body[i].draw()
        }
    }
    
    
    function isHit(snake) {
        const head = snake.head
        // 是否碰到左右邊界
        const xLimit = head.x < 0 || head.x >= canvas.width
        // 是否碰到上下邊界
        const yLimit = head.y < 0 || head.y >= canvas.height
        // 是否撞到蛇身
        const hitSelf = snake.body.find(({ x, y }) => head.x === x && head.y === y)
        // 三者其中一個為true則遊戲結束
        return xLimit || yLimit || hitSelf
    }

自此,貪吃蛇?小遊戲完成嘍:
貪吃蛇.gif

6. 全部程式碼:


draw()

function draw() {
    const canvas = document.getElementById('canvas')

    const ctx = canvas.getContext('2d')

    // 定義一個全域性的是否吃到食物的一個變數
    let isEatFood = false

    // 小方格的建構函式
    function Rect(x, y, width, height, color) {
        this.x = x
        this.y = y
        this.width = width
        this.height = height
        this.color = color
    }

    Rect.prototype.draw = function () {
        ctx.beginPath()
        ctx.fillStyle = this.color
        ctx.fillRect(this.x, this.y, this.width, this.height)
        ctx.strokeRect(this.x, this.y, this.width, this.height)
    }

    // 蛇的建構函式
    function Snake(length = 0) {

        this.length = length
        // 蛇頭
        this.head = new Rect(canvas.width / 2, canvas.height / 2, 40, 40, 'red')

        // 蛇身
        this.body = []

        let x = this.head.x - 40
        let y = this.head.y

        for (let i = 0; i < this.length; i++) {
            const rect = new Rect(x, y, 40, 40, 'yellow')
            this.body.push(rect)
            x -= 40
        }
    }

    Snake.prototype.drawSnake = function () {
        // 如果碰到了
        if (isHit(this)) {
            // 清除定時器
            clearInterval(timer)
            const con = confirm(`總共吃了${this.body.length - this.length}個食物,重新開始嗎`)
            // 是否重開
            if (con) {
                draw()
            }
            return
        }
        // 繪製蛇頭
        this.head.draw()
        // 繪製蛇身
        for (let i = 0; i < this.body.length; i++) {
            this.body[i].draw()
        }
    }

    Snake.prototype.moveSnake = function () {
        // 將蛇頭上一次狀態,拼到蛇身首部
        const rect = new Rect(this.head.x, this.head.y, this.head.width, this.head.height, 'yellow')
        this.body.unshift(rect)

        // 判斷蛇頭是否與食物重疊,重疊就是吃到了,沒重疊就是沒吃到
        isEatFood = food && this.head.x === food.x && this.head.y === food.y

        // 我們們上面在蛇身首部插入方格
        if (!isEatFood) {
            // 沒吃到就要去尾,相當於整條蛇沒變長
            this.body.pop()
        } else {
            // 吃到了就不去尾,相當於整條蛇延長一個方格

            // 並且吃到了,就要重新生成一個隨機食物
            food = randomFood(this)
            food.draw()
            isEatFood = false
        }

        // 根據方向,控制蛇頭的座標
        switch (this.direction) {
            case 0:
                this.head.x -= this.head.width
                break
            case 1:
                this.head.y -= this.head.height
                break
            case 2:
                this.head.x += this.head.width
                break
            case 3:
                this.head.y += this.head.height
                break
        }
    }

    document.onkeydown = function (e) {
        // 鍵盤事件
        e = e || window.event
        // 左37  上38  右39  下40
        switch (e.keyCode) {
            case 37:
                console.log(37)
                // 三元表示式,防止右移動時按左,下面同理(貪吃蛇可不能直接掉頭)
                snake.direction = snake.direction === 2 ? 2 : 0
                snake.moveSnake()
                break
            case 38:
                console.log(38)
                snake.direction = snake.direction === 3 ? 3 : 1
                break
            case 39:
                console.log(39)
                snake.direction = snake.direction === 0 ? 0 : 2
                break
            case 40:
                console.log(40)
                snake.direction = snake.direction === 1 ? 1 : 3
                break

        }
    }

    function randomFood(snake) {
        let isInSnake = true
        let rect
        while (isInSnake) {
            const x = Math.round(Math.random() * (canvas.width - 40) / 40) * 40
            const y = Math.round(Math.random() * (canvas.height - 40) / 40) * 40
            console.log(x, y)
            // 保證是40的倍數啊
            rect = new Rect(x, y, 40, 40, 'blue')
            // 判斷食物是否與蛇頭蛇身重疊
            if ((snake.head.x === x && snake.head.y === y) || snake.body.find(item => item.x === x && item.y === y)) {
                isInSnake = true
                continue
            } else {
                isInSnake = false
            }
        }
        return rect
    }

    function isHit(snake) {
        const head = snake.head
        // 是否碰到左右邊界
        const xLimit = head.x < 0 || head.x >= canvas.width
        // 是否碰到上下邊界
        const yLimit = head.y < 0 || head.y >= canvas.height
        // 是否撞到蛇身
        const hitSelf = snake.body.find(({ x, y }) => head.x === x && head.y === y)
        // 三者其中一個為true則遊戲結束
        return xLimit || yLimit || hitSelf
    }

    const snake = new Snake(3)
    // 預設direction為2,也就是右
    snake.direction = 2
    snake.drawSnake()
    // 建立隨機食物例項
    var food = randomFood(snake)
    // 畫出食物
    food.draw()

    function animate() {
        // 先清空
        ctx.clearRect(0, 0, canvas.width, canvas.height)
        // 移動
        snake.moveSnake()
        // 再畫
        snake.drawSnake()
        food.draw()
    }

    var timer = setInterval(() => {
        animate()
    }, 100)
}

星星連線

效果如下,是不是很酷炫呢,兄弟們(背景圖片 可以自己去下載一下):

星星連線.gif

這個小遊戲可分為以下幾步:

  • 1、畫出單個小星星並使他移動
  • 2、造出一百個小星星
  • 3、星星之間靠近時,進行連線
  • 4、滑鼠移動生成小星星
  • 5、滑鼠點選產生5個小星星

1. 畫出單個小星星,並使他移動

其實移動星星很簡單,就是清除後重新繪製星星,並利用定時器,就會有移動的視覺了。注意點在於:碰到邊界要反彈

// html
<style>
    #canvas {
            background: url(./光能使者.jpg) 0 0/cover no-repeat;
        }
</style>
<canvas id="canvas"></canvas>

// js

const canvas = document.getElementById('canvas')

const ctx = canvas.getContext('2d')

// 獲取當前檢視的寬度和高度
let aw = document.documentElement.clientWidth || document.body.clientWidth
let ah = document.documentElement.clientHeight || document.body.clientHeight
// 賦值給canvas
canvas.width = aw
canvas.height = ah

// 螢幕變動時也要監聽實時寬高
window.onresize = function () {
    aw = document.documentElement.clientWidth || document.body.clientWidth
    ah = document.documentElement.clientHeight || document.body.clientHeight
    // 賦值給canvas
    canvas.width = aw
    canvas.height = ah
}

// 本遊戲無論是實心,還是線條,色調都是白色
ctx.fillStyle = 'white'
ctx.strokeStyle = 'white'

function Star(x, y, r) {
    // x,y是座標,r是半徑
    this.x = x
    this.y = y
    this.r = r
    // speed引數,在  -3 ~ 3 之間取值
    this.speedX = (Math.random() * 3) * Math.pow(-1, Math.round(Math.random()))
    this.speedY = (Math.random() * 3) * Math.pow(-1, Math.round(Math.random()))
}

Star.prototype.draw = function () {
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2)
    ctx.fill()
    ctx.closePath()
}

Star.prototype.move = function () {
    this.x -= this.speedX
    this.y -= this.speedY
    // 碰到邊界時,反彈,只需要把speed取反就行
    if (this.x < 0 || this.x > aw) this.speedX *= -1
    if (this.y < 0 || this.y > ah) this.speedY *= -1
}

// 隨機在canvas範圍內找一個座標畫星星
const star = new Star(Math.random() * aw, Math.random() * ah, 3)
star

// 星星的移動
setInterval(() => {
    ctx.clearRect(0, 0, aw, ah)
    star.move()
    star.draw()
}, 50)

達到以下移動以及反彈的效果:

星星反彈.gif

2、畫100個小星星

建立一個陣列stars來儲存這些星星

const stars = []
for (let i = 0; i < 100; i++) {
    // 隨機在canvas範圍內找一個座標畫星星
    stars.push(new Star(Math.random() * aw, Math.random() * ah, 3))
}

// 星星的移動
setInterval(() => {
    ctx.clearRect(0, 0, aw, ah)
    // 遍歷移動渲染
    stars.forEach(star => {
        star.move()
        star.draw()
    })
}, 50)

效果如下:

100個星星.gif

3. 星星之間靠近時,進行連線

當兩個星星的x和y相差都小於50時,就進行連線,連線只需要使用ctx.moveTo和ctx.lineTo就可以了

function drawLine(startX, startY, endX, endY) {
    ctx.beginPath()
    ctx.moveTo(startX, startY)
    ctx.lineTo(endX, endY)
    ctx.stroke()
    ctx.closePath()
}

// 星星的移動
setInterval(() => {
    ctx.clearRect(0, 0, aw, ah)
    // 遍歷移動渲染
    stars.forEach(star => {
        star.move()
        star.draw()
    })
    stars.forEach((star, index) => {
        // 類似於氣泡排序那樣,去比較,確保所有星星兩兩之間都比較到
        for (let i = index + 1; i < stars.length; i++) {
            if (Math.abs(star.x - stars[i].x) < 50 && Math.abs(star.y - stars[i].y) < 50) {
                drawLine(star.x, star.y, stars[i].x, stars[i].y)
            }
        }
    })
}, 50)

大家可以想一想,為什麼兩個forEach不能何在一起去執行。這是個值得思考的問題,或者大家可以合併在一起執行,試試效果,獲取就懂了。算是給大家留的一個作業哈!

效果如下:

連線星星.gif

4.滑鼠移動時帶著小星星

也就是滑鼠到哪,那個小星星就到哪,並且這個小星星走到哪都會跟距離近的小星星連線

const mouseStar = new Star(0, 0, 3)

canvas.onmousemove = function (e) {
    mouseStar.x = e.clientX
    mouseStar.y = e.clientY
}

// 星星的移動
setInterval(() => {
    ctx.clearRect(0, 0, aw, ah)
    // 滑鼠星星渲染
    mouseStar.draw()
    // 遍歷移動渲染
    stars.forEach(star => {
        star.move()
        star.draw()
    })
    stars.forEach((star, index) => {
        // 類似於氣泡排序那樣,去比較,確保所有星星兩兩之間都比較到
        for (let i = index + 1; i < stars.length; i++) {
            if (Math.abs(star.x - stars[i].x) < 50 && Math.abs(star.y - stars[i].y) < 50) {
                drawLine(star.x, star.y, stars[i].x, stars[i].y)
            }
        }
        // 判斷滑鼠星星連線
        if (Math.abs(mouseStar.x - star.x) < 50 && Math.abs(mouseStar.y - star.y) < 50) {
            drawLine(mouseStar.x, mouseStar.y, star.x, star.y)
        }
    })
}, 50)

效果如下:

滑鼠星星.gif

5. 滑鼠點選生成五個小星星

思路就是,滑鼠點選,生成5個小星星,並加到陣列stars

window.onclick = function (e) {
    for (let i = 0; i < 5; i++) {
        stars.push(new Star(e.clientX, e.clientY, 3))
    }
}

效果如下:

點選生成星星.gif

最終效果:
星星連線.gif

6. 全部程式碼

const canvas = document.getElementById('canvas')

const ctx = canvas.getContext('2d')

// 獲取當前檢視的寬度和高度
let aw = document.documentElement.clientWidth || document.body.clientWidth
let ah = document.documentElement.clientHeight || document.body.clientHeight
// 賦值給canvas
canvas.width = aw
canvas.height = ah

// 螢幕變動時也要監聽實時寬高
window.onresize = function () {
    aw = document.documentElement.clientWidth || document.body.clientWidth
    ah = document.documentElement.clientHeight || document.body.clientHeight
    // 賦值給canvas
    canvas.width = aw
    canvas.height = ah
}

// 本遊戲無論是實心,還是線條,色調都是白色
ctx.fillStyle = 'white'
ctx.strokeStyle = 'white'

function Star(x, y, r) {
    // x,y是座標,r是半徑
    this.x = x
    this.y = y
    this.r = r
    // speed引數,在  -3 ~ 3 之間取值
    this.speedX = (Math.random() * 3) * Math.pow(-1, Math.round(Math.random()))
    this.speedY = (Math.random() * 3) * Math.pow(-1, Math.round(Math.random()))
}

Star.prototype.draw = function () {
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2)
    ctx.fill()
    ctx.closePath()
}

Star.prototype.move = function () {
    this.x -= this.speedX
    this.y -= this.speedY
    // 碰到邊界時,反彈,只需要把speed取反就行
    if (this.x < 0 || this.x > aw) this.speedX *= -1
    if (this.y < 0 || this.y > ah) this.speedY *= -1
}

function drawLine(startX, startY, endX, endY) {
    ctx.beginPath()
    ctx.moveTo(startX, startY)
    ctx.lineTo(endX, endY)
    ctx.stroke()
    ctx.closePath()
}

const stars = []
for (let i = 0; i < 100; i++) {
    // 隨機在canvas範圍內找一個座標畫星星
    stars.push(new Star(Math.random() * aw, Math.random() * ah, 3))
}

const mouseStar = new Star(0, 0, 3)

canvas.onmousemove = function (e) {
    mouseStar.x = e.clientX
    mouseStar.y = e.clientY
}
window.onclick = function (e) {
    for (let i = 0; i < 5; i++) {
        stars.push(new Star(e.clientX, e.clientY, 3))
    }
}

// 星星的移動
setInterval(() => {
    ctx.clearRect(0, 0, aw, ah)
    // 滑鼠星星渲染
    mouseStar.draw()
    // 遍歷移動渲染
    stars.forEach(star => {
        star.move()
        star.draw()
    })
    stars.forEach((star, index) => {
        // 類似於氣泡排序那樣,去比較,確保所有星星兩兩之間都比較到
        for (let i = index + 1; i < stars.length; i++) {
            if (Math.abs(star.x - stars[i].x) < 50 && Math.abs(star.y - stars[i].y) < 50) {
                drawLine(star.x, star.y, stars[i].x, stars[i].y)
            }
        }

        if (Math.abs(mouseStar.x - star.x) < 50 && Math.abs(mouseStar.y - star.y) < 50) {
            drawLine(mouseStar.x, mouseStar.y, star.x, star.y)
        }
    })
}, 50)

3. 五子棋

看看將實現的效果:

截圖2021-07-25 下午12.21.39.png
五子棋分為以下步驟:

  • 1、畫出棋盤
  • 2、黑白棋切換著下,不能覆蓋已下的坑位
  • 3、判斷是否五連子,是的話就贏了
  • 4、彩蛋:跟AI下棋(實現單人玩遊戲)

1. 畫出棋盤

其實很簡單,利用ctx.moveTo和ctx.lineTo,橫著畫15條線,豎著畫15條線,就OK了。

// html
#canvas {
            background: #e3cdb0;
        }
<canvas id="canvas" width="600" height="600"></canvas>


// js
play()

function play() {
    const canvas = document.getElementById('canvas')

    const ctx = canvas.getContext('2d')

    // 繪製棋盤

    // 水平,總共15條線
    for (let i = 0; i < 15; i++) {
        ctx.beginPath()
        ctx.moveTo(20, 20 + i * 40)
        ctx.lineTo(580, 20 + i * 40)
        ctx.stroke()
        ctx.closePath()
    }

    // 垂直,總共15條線
    for (let i = 0; i < 15; i++) {
        ctx.beginPath()
        ctx.moveTo(20 + i * 40, 20)
        ctx.lineTo(20 + i * 40, 580)
        ctx.stroke()
        ctx.closePath()
    }
}

這樣就畫出了棋盤:

截圖2021-07-25 下午12.25.09.png

2. 黑白棋切換著下

  • 1、滑鼠點選事件,獲取座標,將棋畫出來(ctx.arc
  • 2、確保已下的棋位不能重複下

第一步,獲取滑鼠座標,但是我們要注意一件事,棋子只能下線上的交叉處,所以拿到滑鼠座標後,要做一下處理,四捨五入,以最近的一個線交叉點為圓的圓心

第二步,如何確保棋位不重複下呢?我們們可以使用一個二維陣列來記錄,初始是0,下過黑棋就變為1,下過白棋就變為2,但是這裡要注意一點,陣列索引的x,y跟畫布座標的x,y是相反的,所以後面程式碼裡座標反過來,希望大家能思考一下為啥。

截圖2021-07-25 下午12.33.29.png

// 是否下黑棋
    // 黑棋先走
    let isBlack = true


    // 棋盤二維陣列
    let cheeks = []

    for (let i = 0; i < 15; i++) {
        cheeks[i] = new Array(15).fill(0)
    }

    canvas.onclick = function (e) {
        const clientX = e.clientX
        const clientY = e.clientY
        // 對40進行取整,確保棋子落在交叉處
        const x = Math.round((clientX - 20) / 40) * 40 + 20
        const y = Math.round((clientY - 20) / 40) * 40 + 20
        // cheeks二維陣列的索引
        // 這麼寫有點冗餘,這麼寫你們好理解一點
        const cheeksX = (x - 20) / 40
        const cheeksY = (y - 20) / 40
        // 對應元素不為0說明此地方已有棋,返回
        if (cheeks[cheeksY][cheeksX]) return
        // 黑棋為1,白棋為2
        cheeks[cheeksY][cheeksX] = isBlack ? 1 : 2
        ctx.beginPath()
        // 畫圓
        ctx.arc(x, y, 20, 0, 2 * Math.PI)
        // 判斷走黑還是白
        ctx.fillStyle = isBlack ? 'black' : 'white'
        ctx.fill()
        ctx.closePath()
        // 切換黑白
        isBlack = !isBlack
    }

下棋.gif
效果如下:

3. 判斷是否五連子

如何判斷呢?有四種情況:上下五連子,左右吳連子,左上右下五連子,右上左下五連子,只要我們們每次落子的時候全部判斷一次就好了。

截圖2021-07-25 下午12.55.53.png

順便附上所有程式碼

play()

function play() {
    const canvas = document.getElementById('canvas')

    const ctx = canvas.getContext('2d')

    // 繪製棋盤

    // 水平,總共15條線
    for (let i = 0; i < 15; i++) {
        ctx.beginPath()
        ctx.moveTo(20, 20 + i * 40)
        ctx.lineTo(580, 20 + i * 40)
        ctx.stroke()
        ctx.closePath()
    }

    // 垂直,總共15條線
    for (let i = 0; i < 15; i++) {
        ctx.beginPath()
        ctx.moveTo(20 + i * 40, 20)
        ctx.lineTo(20 + i * 40, 580)
        ctx.stroke()
        ctx.closePath()
    }

    // 是否下黑棋
    // 黑棋先走
    let isBlack = true


    // 棋盤二維陣列
    let cheeks = []

    for (let i = 0; i < 15; i++) {
        cheeks[i] = new Array(15).fill(0)
    }

    canvas.onclick = function (e) {
        const clientX = e.clientX
        const clientY = e.clientY
        // 對40進行取整,確保棋子落在交叉處
        const x = Math.round((clientX - 20) / 40) * 40 + 20
        const y = Math.round((clientY - 20) / 40) * 40 + 20
        // cheeks二維陣列的索引
        // 這麼寫有點冗餘,這麼寫你們好理解一點
        const cheeksX = (x - 20) / 40
        const cheeksY = (y - 20) / 40
        // 對應元素不為0說明此地方已有棋,返回
        if (cheeks[cheeksY][cheeksX]) return
        // 黑棋為1,白棋為2
        cheeks[cheeksY][cheeksX] = isBlack ? 1 : 2
        ctx.beginPath()
        // 畫圓
        ctx.arc(x, y, 20, 0, 2 * Math.PI)
        // 判斷走黑還是白
        ctx.fillStyle = isBlack ? 'black' : 'white'
        ctx.fill()
        ctx.closePath()

        // canvas畫圖是非同步的,保證畫出來再去檢測輸贏
        setTimeout(() => {
            if (isWin(cheeksX, cheeksY)) {
                const con = confirm(`${isBlack ? '黑棋' : '白棋'}贏了!是否重新開局?`)
                // 重新開局
                ctx.clearRect(0, 0, 600, 600)
                con && play()
            }
            // 切換黑白
            isBlack = !isBlack
        }, 0)
    }
    // 判斷是否五連子
    function isWin(x, y) {
        const flag = isBlack ? 1 : 2
        // 上和下
        if (up_down(x, y, flag)) {
            return true
        }

        // 左和右
        if (left_right(x, y, flag)) {
            return true
        }
        // 左上和右下
        if (lu_rd(x, y, flag)) {
            return true
        }

        // 右上和左下
        if (ru_ld(x, y, flag)) {
            return true
        }

        return false
    }

    function up_down(x, y, flag) {
        let num = 1
        // 向上找
        for (let i = 1; i < 5; i++) {
            let tempY = y - i
            console.log(x, tempY)
            if (tempY < 0 || cheeks[tempY][x] !== flag) break
            if (cheeks[tempY][x] === flag) num += 1
        }
        // 向下找
        for (let i = 1; i < 5; i++) {
            let tempY = y + i
            console.log(x, tempY)
            if (tempY > 14 || cheeks[tempY][x] !== flag) break
            if (cheeks[tempY][x] === flag) num += 1
        }
        return num >= 5
    }

    function left_right(x, y, flag) {
        let num = 1
        // 向左找
        for (let i = 1; i < 5; i++) {
            let tempX = x - i
            if (tempX < 0 || cheeks[y][tempX] !== flag) break
            if (cheeks[y][tempX] === flag) num += 1
        }
        // 向右找
        for (let i = 1; i < 5; i++) {
            let tempX = x + i
            if (tempX > 14 || cheeks[y][tempX] !== flag) break
            if (cheeks[y][tempX] === flag) num += 1
        }
        return num >= 5

    }

    function lu_rd(x, y, flag) {
        let num = 1
        // 向左上找
        for (let i = 1; i < 5; i++) {
            let tempX = x - i
            let tempY = y - i
            if (tempX < 0 || tempY < 0 || cheeks[tempY][tempX] !== flag) break
            if (cheeks[tempY][tempX] === flag) num += 1
        }
        // 向右下找
        for (let i = 1; i < 5; i++) {
            let tempX = x + i
            let tempY = y + i
            if (tempX > 14 || tempY > 14 || cheeks[tempY][tempX] !== flag) break
            if (cheeks[tempY][tempX] === flag) num += 1
        }

        return num >= 5
    }

    function ru_ld(x, y, flag) {
        let num = 1
        // 向右上找
        for (let i = 1; i < 5; i++) {
            let tempX = x - i
            let tempY = y + i
            if (tempX < 0 || tempY > 14 || cheeks[tempY][tempX] !== flag) break
            if (cheeks[tempY][tempX] === flag) num += 1
        }
        // 向左下找
        for (let i = 1; i < 5; i++) {
            let tempX = x + i
            let tempY = y - i
            if (tempX > 14 || tempY < 0 || cheeks[tempY][tempX] !== flag) break
            if (cheeks[tempY][tempX] === flag) num += 1
        }

        return num >= 5
    }

}

4. 彩蛋:與AI下棋

其實很簡單,每次下完棋,設定一個函式:隨機找位置下棋。這樣就實現了和電腦下棋,單人遊戲的功能了,這個功能我已經實現,但是我就不寫出來了,交給大家吧,當做是大家鞏固這篇文章的作業。哈哈哈哈

結語

我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群,加我請備註【思否】

image.png

相關文章