诸葛单人棋

149 阅读3分钟

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
  <title>Document</title>
  <style>
    html, body {
      height: 100%;
    }
    body {
      display: flex;
      flex-direction: column;
      justify-content: center;
    }
    canvas {
      user-select: none;
      position: absolute;
    }
    #stage {
      position: relative;
      height: auto;
    }
    #background-layer { z-index: 1 }
    #buff-layer { z-index: 2 }
    #moving-buff-layer { z-index: 3 }
    #playground { z-index: 4 }

    .controller {
      display: flex;
      justify-content: space-around;
      margin-bottom: 12px;;
    }
  </style>
</head>
<body>
  <div class="controller">
    <button onclick="game.destroy()">reset</button>
    <button onclick="game.snapShotStack.backward()">上一步</button>
    <button onclick="game.snapShotStack.forward()">下一步</button>
  </div>
  <!-- <p>
    玩法 全部棋子摆满后,取出中央的一颗。棋子只能横向纵向(不可斜向)跳过邻的棋子到空位上。被跳过的棋子即被吃掉。<br/>
    按照前面的方法继续移动,直到最后走不下去了,数数还剩下几颗棋子。剩下的棋子越少越厉害。
  </p> -->
  <div id="stage">
    <canvas id="background-layer"></canvas>
    <canvas id="buff-layer"></canvas>
    <canvas id="moving-buff-layer"></canvas>
    <canvas id="playground" onselectstart="return false;"></canvas>
  </div>

  <script>
    function Position (x, y) {
      this.x = x; this.y = y;
    }

    Position.prototype.set = function (x, y) {
      this.x = x; this.y = y;
    }

    Position.prototype.get = function () {
      return [this.x, this.y]
    }

    function get (obj, ...props) {
      let res = obj
      while (res && props.length > 0) {
        const key = props.shift()
        if (key in res) {
          res = res[key]
          if (props.length === 0) {
            return res
          }
        } else {
          return
        }
      }
    }
  </script>
  <script>
    function Chequer (ctx, position, size) {
      this.ctx = ctx
      this.position = position
      this.selected = false
      this.heart = new Position(
        this.position.x +  size / 2,
        this.position.y +  size / 2
      )
      this.render()
    }

    Chequer.prototype.setHeart = function (heart) {
      this.heart.x = heart.x
      this.heart.y = heart.y
      return this
    }

    Chequer.prototype.toggleSelect = function (selected) {
      if (typeof selected === 'boolean') {
        this.selected = selected
      } else {
        this.selected = !this.selected
      }
      this.render()
    }

    Chequer.prototype.render = function () {
      this.clear()
      this.ctx.beginPath()
      const heart = this.heart
      this.ctx.arc(heart.x, heart.y, Chequer.radius, 0, Math.PI * 2, true);
      if (this.selected) {
        this.ctx.fill()
      } else {
        this.ctx.stroke()
      }

      const r = Chequer.radius * 0.618
      this.ctx.beginPath()
      this.ctx.save()
      this.ctx.fillStyle = this.selected ? '#fff' : '#000'
      this.ctx.arc(heart.x, heart.y, r, 0, Math.PI * 2, true)
      this.ctx.fill()
      this.ctx.restore()
    }

    Chequer.prototype.clear = function () {
      const lineWidth = this.ctx.lineWidth
      const diameter = Chequer.radius * 2 + 2 * lineWidth
      this.ctx.clearRect(this.heart.x - Chequer.radius - lineWidth, this.heart.y - Chequer.radius - lineWidth, diameter, diameter)
      this.ctx.beginPath()
    }

    Chequer.radius = 12
  </script>
  <script>
    function Lattice (ctx, position) {
      const size = Chequer.radius * 2 + Lattice.padding * 2
      this.ctx = ctx
      this.size = size
      this.x = position.x
      this.y = position.y

      position.x *= size
      position.y *= size
      this.position = position
      this.selected = false
    }

    Lattice.prototype.clear = function () {
      this.ctx.clearRect(this.position.x, this.position.y, this.size, this.size)
    }

    Lattice.padding = 10
    Lattice.lineWidth = 1

    function ChequerLattice (ctx, position, playgroundCtx, isRender = true) {
      Lattice.call(this, ctx, position)
      this.chequer = new Chequer(playgroundCtx, position, this.size)
      isRender && this.render()
    }

    ChequerLattice.prototype.render = function () {
      const { ctx, position, size } = this
      ctx.beginPath()
      ctx.rect(position.x, position.y, size, size)
      ctx.stroke()
    }

    ChequerLattice.prototype.toggleSelect = function (selected) {
      if (typeof selected === 'boolean') {
        this.selected = selected
      } else {
        this.selected = !this.selected
      }
      this.chequer.toggleSelect(selected)
    }

    function extend (targetCtor, ...sourceCtors) {
      for (var i = 0; i < sourceCtors.length; i++) {
        const sourceCtor = sourceCtors[i];
        const oldPrototype = sourceCtor.prototype
        sourceCtor.prototype = Object.assign(Object.create(targetCtor.prototype), oldPrototype)
        sourceCtor.prototype.constructor = sourceCtor
      }
    }
    extend(Lattice, ChequerLattice)

    function Chessboard (ctx) {
      const size = (Chequer.radius * 2 + Lattice.padding * 2) * 7 + 8 * Lattice.lineWidth
      this.size = size
      this.ctx = ctx
    }

    Chessboard.prototype.create = function () {
      ctx.beginPath()

      ctx.rect(0, 0, this.size, this.size);

      ctx.stroke()
    }
  </script>
  <script>
    function SnapShot (instance) {
        this.ctor = instance.constructor
        const position = instance.position
        const size = instance.size
        this.position = new Position(position.x / size, position.y / size)
      }

      function SnapShotBuilder () {
        this._from = null
        this._to = null
        this._path = null
      }

      SnapShotBuilder.prototype.from = function (from) {
        this._from = from
      }

      SnapShotBuilder.prototype.to = function (to) {
        this._to = to
      }

      SnapShotBuilder.prototype.path = function (path) {
        this._path = path
      }

      SnapShotBuilder.prototype.build = function () {
        return {
          from: this._from,
          to: this._to,
          path: this._path
        }
      }

      function SnapShotStack (game) {
        this.index = -1
        this.game = game
        this.stacks = []
      }

      SnapShotStack.prototype.push = function (snapshot) {
        console.info(`
          'from', ${snapshot.from.ctor.name} ${snapshot.from.position.x}, ${snapshot.from.position.y},
          'path', ${snapshot.path.ctor.name} ${snapshot.path.position.x}, ${snapshot.path.position.y},
          'to', ${snapshot.to.ctor.name} ${snapshot.to.position.x}, ${snapshot.to.position.y}
        `)
        const len = this.stacks.length
        if (len !== this.index + 1) {
          this.stacks.splice(this.index, this.stacks.length, snapshot)
        } else {
          this.stacks.push(snapshot)
        }
        this.index++
      }

      SnapShotStack.prototype.forward = function () {
        if (this.index >= this.stacks.length) {
          console.warn("SnapShotStack", )
          return
        }
        this.index++
        this.do(true)
      }

      SnapShotStack.prototype.backward = function () {
        console.info(this.message())
        if (this.index < 0) {
          console.warn("none can be revert")
          return
        }
        this.do()
        this.index--
      }

      SnapShotStack.prototype.do = function (isForward) {
        const stack = this.stacks[this.index]

        for (const type in stack) {
          const Ctor = stack[type].ctor
          const { x, y } = stack[type].position
          let lattice
          this.game.lattices[x][y]?.chequer?.clear()

          const params = [this.game.bgCtx, new Position(x, y), this.game.playgroundCtx]

          if (isForward) {
            lattice = Ctor === Lattice ? new Lattice(...params) : new ChequerLattice(...params, false)
          } else {
            lattice = Ctor === Lattice ? new ChequerLattice(...params, false) : new Lattice(...params)
          }
          this.game.lattices[x][y] = lattice
        }
      }

      SnapShotStack.prototype.message = function () {
        return `length: ${this.stacks.length}; index: ${this.index}`
      }
  </script>
  <script>
     function Game () {
      this.chessboard = this.createBroad()
      this.latticeSize = 7
      this.lattices = []
      this.selectedLattice = null
      this.mousedownHandler = null
      this.bgCtx = null
      this.playgroundCtx = null
      this.buffCtx = null
      this.buffedAreas = []
      this.movingBuffCtx = null
      this.snapShotStack = new SnapShotStack(this)
    }

    Game.prototype.init = function () {
      document.getElementById("stage").style.height = this.chessboard.size + 'px'

      this.selectedChequer = null
      this.initLattices()
      this.initHandlers()
      this.reset()
      return this
    }

    Game.prototype.initLattices = function () {
      this.lattices = new Array(this.latticeSize).fill(null).map(v => new Array(this.latticeSize).fill(null))
    }

    Game.prototype.initHandlers = function () {
      this.mousedownHandler = this.mousedown.bind(this)
      const canvas = document.getElementById('playground')

      canvas.addEventListener('mousedown', this.mousedownHandler)
    }

    Game.prototype.reset = function () {
      const size = this.chessboard.size

      {
        const canvas = document.getElementById('background-layer')
        this.bgCtx= canvas.getContext('2d')
        canvas.width = size
        canvas.height = size
      }

      {
        const canvas = document.getElementById('buff-layer')
        this.buffCtx= canvas.getContext('2d')
        canvas.width = size
        canvas.height = size
      }

      {
        const canvas = document.getElementById('moving-buff-layer')
        this.movingBuffCtx= canvas.getContext('2d')
        canvas.width = size
        canvas.height = size
      }

      {
        const canvas = document.getElementById('playground')
        canvas.width = size
        canvas.height = size
        this.playgroundCtx = canvas.getContext('2d')
      }

      this.resetPlayground()
    }

    Game.prototype.createBuffLayer = function (lattice) {
      const { position, size } = lattice
      const [x, y] = [position.x / size, position.y / size]

      const tryGet = (x, y) => {
        const rows = this.lattices[x]
        if (rows) {
          return rows[y]
        }
      }

      const up = (step = 1) => tryGet(x, y - step)
      const down = (step = 1) => tryGet(x, y + step)
      const left = (step = 1) => tryGet(x - step, y)
      const right = (step = 1) => tryGet(x + step, y)
      const buffed = []
      if (up()?.chequer) {
        const far = up(2)
        if (far && !far.chequer) {
          buffed.push(far)
        }
      }

      if (down()?.chequer) {
        const far = down(2)
        if (far && !far.chequer) {
          buffed.push(far)
        }
      }

      if (left()?.chequer) {
        const far = left(2)
        if (far && !far.chequer) {
          buffed.push(far)
        }
      }

      if (right()?.chequer) {
        const far = right(2)
        if (far && !far.chequer) {
          buffed.push(far)
        }
      }
      buffed.forEach((lattice) => this.renderBuff(lattice))
    }

    Game.prototype.renderBuff = function (lattice) {
      const { position, size } = lattice
      const ctx = this.buffCtx
      const gradient = ctx.createLinearGradient(position.x , position.y, position.x + size, position.y);

      // Add three color stops
      gradient.addColorStop(0, "lightgreen");
      gradient.addColorStop(0.5, "#c8f8c8");
      gradient.addColorStop(1, "lightgreen");

      // Set the fill style and draw a rectangle
      ctx.fillStyle = gradient;
      const params = [position.x, position.y, size, size]
      this.buffedAreas.push(lattice)
      ctx.fillRect(...params);
    }

    Game.prototype.clearBuffLayer = function () {
      const ctx = this.buffCtx
      ctx.beginPath()
      for (let i = this.buffedAreas.length - 1; i >= 0; i--) {
        const { position, size } = this.buffedAreas[i]
        const params = [position.x, position.y, size, size]
        ctx.clearRect(...params)
        this.buffedAreas.splice(i, 1)
      }
    }

    Game.prototype.resetPlayground = function () {
      const bgCtx = this.bgCtx

      for (let i = 0 ; i < this.latticeSize; i++) {
        for (let j = 0 ; j < this.latticeSize; j++) {
          const position = new Position(i, j)
          if (
            ((i < 2 || i > 4) && j < 2) ||
            ((i < 2 || i > 4) && j > 4)
          ) {
            continue
          }

          if (i === 3 && j === 3) {
            this.lattices[i][j] = new Lattice(bgCtx, position, this.playgroundCtx)
          } else {
            this.lattices[i][j] = new ChequerLattice(bgCtx, position, this.playgroundCtx)
          }
        }
      }
    }

    Game.prototype.createBroad = function () {
      const canvas = document.getElementById('background-layer')
      const ctx = canvas.getContext('2d')
      this.ctx = ctx
      return new Chessboard(ctx)
    }

    Game.prototype.destroy = function () {
      this.clearBuffLayer()
      this.lattices.flat().forEach(lattice => lattice?.chequer?.clear())

      for (let i = 0; i < this.lattices.length; i++) {
        const rows = this.lattices[i]
        for (let j = 0 ; j < rows.length; j++) {
          rows[j] = null
        }
      }

      this.selectedLattice = null
      this.resetPlayground()
    }

    Game.prototype.toggleSelect = function (lattice, isSelected) {
      lattice.toggleSelect(isSelected)
      if (isSelected) {
        this.createBuffLayer(lattice)
      } else {
        this.clearBuffLayer()
      }
    }

    Game.prototype.mousedown = function (e) {
      const { offsetX, offsetY } = e
      let isInBuffedArea = false
      rowsLoop:
      for (let i = 0; i < this.lattices.length; i++) {
        const rows = this.lattices[i]
        colsLoop:
        for (let j = 0 ; j < rows.length; j++) {
          const lattice = rows[j]
          // 非格子
          if (!lattice) {
            continue
          }

          if (!lattice.chequer) {
            // 点击非棋子,查看是否吃子
            for (let k = 0; k < this.buffedAreas.length; k++ ) {
              const buffedArea = this.buffedAreas[k];
              isInBuffedArea = offsetX > buffedArea.position.x &&
                offsetY > buffedArea.position.y &&
                offsetX < buffedArea.position.x + buffedArea.size &&
                offsetY < buffedArea.position.y  + buffedArea.size

              if (isInBuffedArea && lattice === buffedArea) {
                isEat = true
                this.clearBuffLayer()
                this.eat(i, j)
                this.judge()
                break rowsLoop
              }
            }

            continue
          }

          // 是否点击棋子
          const chequer = lattice.chequer
          const heart = chequer.heart
          if ((heart.x - offsetX) ** 2 + (heart.y - offsetY) ** 2 < Chequer.radius**2) {
            if (this.selectedLattice === lattice && lattice.selected) {
              this.toggleSelect(this.selectedLattice, false)
              this.selectedLattice = null
              break
            }

            if (this.selectedLattice) {
              this.toggleSelect(this.selectedLattice, false)
            }

            this.toggleSelect(lattice, true)
            this.selectedLattice = lattice
            break rowsLoop
          }
        }
      }
      return false
    }

    // 判断输赢
    Game.prototype.judge = function () {
      const half = Math.floor(this.latticeSize / 2)
      const center = new Position(half, half)

      let restCount = 0
      let restChequers = []
      for (let i = 0; i < this.lattices.length; i++) {
        const rows = this.lattices[i]
        for (let j = 0 ; j < rows.length; j++) {
          if (rows[j]?.chequer) {
            restCount++
            restChequers.push([i, j])
          }
        }
      }

      if (
        restCount === 1 &&
        (restChequers[0].x === center.x && restChequers[0].y === center.y)) {
        console.info('congratulations, you have won the game')
      }
    }

    Game.prototype.hasNeighbor = function (x, y) {
      const bool = get(this.lattices, x - 1, y, 'chequer') ||
        get(this.lattices, x + 1, y, 'chequer') ||
        get(this.lattices, x, y - 1, 'chequer') ||
        get(this.lattices, x, y + 1, 'chequer')
      return bool
    }

    Game.prototype.eat = function (x, y) {
      const snapShotBuilder = new SnapShotBuilder()
      const lattice = this.lattices[x][y]
      // 跳到的地方更新
      this.lattices[x][y] = new ChequerLattice(this.bgCtx, new Position(x, y), this.playgroundCtx, false)

      snapShotBuilder.to(new SnapShot(this.lattices[x][y]))
      if (lattice.position.x === this.selectedLattice.position.x) {
        if (lattice.position.y < this.selectedLattice.position.y) {
          y++
        } else {
          y--
        }
      }

      if (lattice.position.y === this.selectedLattice.position.y) {
        if (lattice.position.x < this.selectedLattice.position.x) {
          x++
        } else {
          x--
        }
      }
      // 跳过的地方清除
      this.lattices[x][y].chequer.clear()
      this.lattices[x][y] = new Lattice(this.bgCtx, new Position(x, y), this.playgroundCtx)
      snapShotBuilder.path(new SnapShot(this.lattices[x][y]))
      // 选中的改变
      x = this.selectedLattice.x
      y = this.selectedLattice.y
      this.lattices[x][y] = new Lattice(this.bgCtx, new Position(x, y), this.playgroundCtx)
      snapShotBuilder.from(new SnapShot(this.lattices[x][y]))
      this.selectedLattice.chequer.clear()
      this.selectedLattice = null

      this.snapShotStack.push(snapShotBuilder.build())
    }

    const game = new Game().init()
  </script>
</body>
</html>