Vue2 simulates Tetris game

Table of contents

1. Effect display

2. Code sharing

3. Principle analysis

3.1. Interface construction

3.2. Block creation

3.3. Block rotation

3.4. Block movement

3.5. Movement judgment

3.6. Determination and removal of whereabouts

3.7. Score calculation

1. Effect display

2. Code sharing

<template>
  <div class="game">
    <div class="game-div">
      <div class="game-min">
        <div class="row" v-for="(row, i) in frame" :key="i">
          <p
            class="element"
            v-for="(col, j) in row"
            :key="j"
            :style="{ background: col.bg }"
          ></p>
        </div>
      </div>
      <!-- 小屏幕 -->
      <div class="right-div">
        <div class="ass">
          <div class="row" v-for="(row, i) in ass" :key="i">
            <p
              class="element"
              v-for="(col, j) in row"
              :key="j"
              :style="{ background: col.bg }"
            ></p>
          </div>
        </div>
        <!-- 分数计算 -->
        <div class="score-div">
          <div>
            <p>
              <span>得分:</span>&emsp;<span style="color: red">{
   
   {
                score
              }}</span>
            </p>
          </div>
          <div>
            <p>
              <span>等级:</span>&emsp;<span style="color: red">{
   
   {
                level
              }}</span>
            </p>
          </div>
          <div>
            <p>
              <span>消除:</span>&emsp;<span style="color: red">{
   
   {
                times
              }}</span>
            </p>
          </div>
          <div class="ztks" @click="stopGame">暂停/开始</div>
        </div>
      </div>
    </div>
    <!-- 控制台 -->
    <div class="control">
      <p @click="change1">变换</p>
      <div class="control-center">
        <div @click="moveLeft">向左</div>
        <div @click="moveRight">向右</div>
      </div>
      <p @click="moveDown">向下</p>
    </div>
  </div>
</template>

<script>
import { color, blockMod, transition } from "@/utils/ykdata.js";
export default {
  data() {
    return {
      row: 20, //行
      col: 10, //列
      frame: [], //界面
      ass: [], //副屏幕
      bg: "#eee",
      block: [], //基本方块集合
      now: { b: 0, c: 0 }, //当前方块以及其旋转角度
      next: { b: 0, c: 0 }, //下一个方块以及其旋转角度
      nowBlock: [], //当前形状数据
      nextBlock: [], //下一个形状数据
      xz: 0, //当前旋转角度
      timer: "", //自动下落
      speed: 800, //速度
      score: 0, //得分
      level: 1, //等级
      times: 0, //消除次数
      stop: true, //是否停止
      removeRow: [], //消除的行记录
    };
  },
  mounted() {
    this.gameFrame();
    this.getBlock(0);
    this.getNext();
    this.init();
  },
  methods: {
    // 游戏框架
    gameFrame() {
      //主屏幕
      for (let i = 0; i < this.row; i++) {
        let a = [];
        for (let j = 0; j < this.col; j++) {
          let b = {
            data: 0,
            bg: this.bg,
          };
          a.push(b);
        }
        this.frame.push(a);
      }
      //副屏幕
      for (let i = 0; i < 4; i++) {
        let a = [];
        for (let j = 0; j < 4; j++) {
          let b = {
            data: 0,
            bg: this.bg,
          };
          a.push(b);
        }
        this.ass.push(a);
      }
      // 模拟格子被占用
      // this.frame[4][4].bg = "#00aaee";
      // this.frame[4][4].data = 1;
    },
    // 渲染方块
    getBlock(e) {
      this.block = blockMod(color[e]);
    },
    // 下一个形状
    async getNext() {
      // 随机获取形状
      this.next.b = Math.floor(Math.random() * this.block.length);
      this.next.c = Math.floor(Math.random() * 4);
    },
    // 渲染当前形状
    init() {
      // 获取到下一个形状数据
      this.now = JSON.parse(JSON.stringify(this.next));
      this.xz = this.now.c;
      // 当前形状数据
      this.nowBlock = JSON.parse(JSON.stringify(this.block[this.now.b]));
      //   渲染形状数据
      //   let c = this.nowBlock.site;
      //   for (let i = 0; i < c.length; i += 2) {
      //     this.frame[c[i]][c[i + 1]].bg = this.nowBlock.color;
      //   }
      this.renderBlock(this.nowBlock, this.frame, 1);
      // 旋转
      if (this.now.c > 0) {
        for (let i = 0; i < this.now.c; i++) {
          this.change(this.nowBlock, this.frame, this.now, i);
        }
      }
      this.getNext().then(() => {
        if (this.nextBlock.site) {
          this.renderBlock(this.nextBlock, this.ass, 0);
        }
        //   下一个形状
        this.nextBlock = JSON.parse(JSON.stringify(this.block[this.next.b]));
        this.renderBlock(this.nextBlock, this.ass, 1);
        // 旋转
        if (this.next.c > 0) {
          for (let i = 0; i < this.next.c; i++) {
            this.change(this.nextBlock, this.ass, this.next, i);
          }
        }
      });
    },
    // 渲染形状
    // b:方块,d:位置,n:0擦除,1生成,2确定落到最下层
    renderBlock(b, d, n) {
      let c = b.site;
      if (n == 0) {
        for (let i = 0; i < c.length; i += 2) {
          d[c[i]][c[i + 1]].bg = this.bg; //0擦除
        }
      } else if (n == 1) {
        for (let i = 0; i < c.length; i += 2) {
          d[c[i]][c[i + 1]].bg = b.color; //1生成
        }
      } else if (n == 2) {
        for (let i = 0; i < c.length; i += 2) {
          d[c[i]][c[i + 1]].data = 1; //2确定落到最下层
        }
      }
    },
    // 旋转 b:当前方块   d:渲染的位置   z:渲染的对象现在还是下一个  xz:当前旋转角度
    change(b, d, z, xz) {
      this.renderBlock(b, d, 0); //先清空再旋转
      // 记录第一块位置
      const x = b.site[0];
      const y = b.site[1];
      for (let i = 0; i < b.site.length; i += 2) {
        let a = b.site[i];
        b.site[i] = b.site[i + 1] - y + x + transition[z.b][xz].x;
        b.site[i + 1] = -(a - x) + y + transition[z.b][xz].y;
      }
      xz++;
      if (xz == 4) {
        xz = 0;
      }
      this.renderBlock(b, d, 1);
    },
    // 向下移动
    moveDown() {
      if (this.canMove(3)) {
        // 先清空
        this.renderBlock(this.nowBlock, this.frame, 0);
        for (let i = 0; i < this.nowBlock.site.length; i += 2) {
          // 向下移动一位
          this.nowBlock.site[i]++;
        }
        this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据
      } else {
        // 已经不能下落了
        this.renderBlock(this.nowBlock, this.frame, 2);
        // 判断是否可以消除
        this.removeBlock();
      }
    },
    // 向左移动
    moveLeft() {
      if (this.canMove(2)) {
        // 先清空
        this.renderBlock(this.nowBlock, this.frame, 0);
        for (let i = 0; i < this.nowBlock.site.length; i += 2) {
          // 向左移动一位
          this.nowBlock.site[i + 1]--;
        }
        this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据
      }
    },
    // 向右移动
    moveRight() {
      if (this.canMove(1)) {
        this.renderBlock(this.nowBlock, this.frame, 0);
        for (let i = 0; i < this.nowBlock.site.length; i += 2) {
          this.nowBlock.site[i + 1]++;
        }
        this.renderBlock(this.nowBlock, this.frame, 1); //再渲染数据
      }
    },
    // 是否可移动判断
    // 预判能否移动或变化   e:1右移  2左移  3下移  4变化
    canMove(e) {
      let c = this.nowBlock.site;
      let d = 0;
      switch (e) {
        case 1:
          for (let i = 0; i < c.length; i += 2) {
            if (c[i + 1] >= this.col - 1) {
              return false;
            }
            // 判断下一个位置是否被占用
            d += this.frame[c[i]][c[i + 1] + 1].data;
          }
          if (d > 0) {
            return false;
          }
          break;
        case 2:
          for (let i = 0; i < c.length; i += 2) {
            if (c[i + 1] <= 0) {
              return false;
            }
            // 判断下一个位置是否被占用
            d += this.frame[c[i]][c[i + 1] - 1].data;
          }
          if (d > 0) {
            return false;
          }
          break;
        case 3:
          for (let i = 0; i < c.length; i += 2) {
            if (c[i] >= this.row - 1) {
              return false;
            }
            // 判断下一个位置是否被占用(防穿透)
            d += this.frame[c[i] + 1][c[i + 1]].data;
          }
          if (d > 0) {
            return false;
          }
          break;
        // case 4:
        //   for (let i = 0; i < c.length; i += 2) {
        //     if (c[i + 1] >= this.col - 1) {
        //       return false;
        //     }
        //   }
        //   break;
      }
      return true;
    },
    // 下落时旋转
    // 旋转 b:当前方块  xz:当前旋转角度
    change1() {
      const b = JSON.parse(JSON.stringify(this.nowBlock));
      // 记录第一块位置
      const x = b.site[0];
      const y = b.site[1];
      let n = true;
      for (let i = 0; i < b.site.length; i += 2) {
        let a = b.site[i];
        b.site[i] = b.site[i + 1] - y + x + transition[this.now.b][this.xz].x;
        b.site[i + 1] = -(a - x) + y + transition[this.now.b][this.xz].y;
        // 判断旋转后该点是否合理
        if (
          b.site[i + 1] < 0 ||
          b.site[i + 1] >= this.col ||
          b.site[i] >= this.row ||
          this.frame[b.site[i]][b.site[i + 1]].data > 0
        ) {
          n = false;
        }
      }
      // 符合条件
      if (n) {
        this.renderBlock(this.nowBlock, this.frame, 0); //先清空
        this.xz++;
        if (this.xz == 4) {
          this.xz = 0;
        }
        this.nowBlock = b;
        this.renderBlock(this.nowBlock, this.frame, 1); //再旋转
      }
    },
    // 到底部:确定位置不能再动,保证上面其他方块下落时不会将它穿透
    // 自动下落
    autoMoveDown() {
      this.timer = setInterval(() => {
        this.moveDown();
      }, this.speed);
    },
    // 开始与暂停
    stopGame() {
      this.stop = !this.stop;
      if (this.stop) {
        clearInterval(this.timer);
      } else {
        this.autoMoveDown();
      }
    },
    // 判断是否可以消除
    removeBlock() {
      // 遍历整个界面
      for (let i = 0; i < this.row; i++) {
        let a = 0;
        for (let j = 0; j < this.col; j++) {
          if (this.frame[i][j].data == 1) {
            a++;
          }
        }
        if (a == this.col) {
          // 说明该行已经占满可以消除
          this.removeRow.push(i);
        }
      }
      // 获取是否可以消除行
      if (this.removeRow.length > 0) {
        let l = this.removeRow;
        for (let i = 0; i < l.length; i++) {
          let j = 0;
          let timer = setInterval(() => {
            this.frame[l[i]][j] = { bg: this.bg, data: 0 };
            j++;
            if (j == this.col) {
              clearInterval(timer);
            }
          }, 20);
        }
        setTimeout(() => {
          // 上面方块下移,从下往上判断
          for (let i = this.row - 1; i >= 0; i--) {
            let a = 0;
            for (let j = 0; j < l.length; j++) {
              if (l[j] > i) {
                a++;
              }
            }
            if (a > 0) {
              for (let k = 0; k < this.col; k++) {
                if (this.frame[i][k].data == 1) {
                  // 先向下移动
                  this.frame[i + a][k] = this.frame[i][k];
                  // 再清除当前
                  this.frame[i][k] = { bg: this.bg, data: 0 };
                }
              }
            }
          }
          this.removeRow = []; //清除行记录
          // 生成下一个
          this.init();
        }, 20 * this.col);
        // 数据处理
        // 消除次数+1
        this.times++;
        // 等级向下取整+1
        let lev =  Math.floor(this.times / 10) + 1;
        if (lev > this.level) {
          this.level = lev;
          // 速度
          if (this.level < 21) {
            // 20级以内做减法
            this.speed = 800 - (this.level - 1) * 40;
          } else {
            this.speed = 30;
          }
          // 清除当前下落
          clearInterval(this.timer);
          // 加速
          this.autoMoveDown();
        }
        this.level = this.times;
        // 分数消除一行100,两行300,三行600,四行1000
        this.score += ((l.length * (l.length + 1)) / 2) * 100 * this.level;
      } else {
        this.init();
      }
    },
  },
};
</script>

<style lang='less' scoped>
.game {
  .game-div {
    display: flex;
    .game-min {
      .row {
        display: flex;
        padding-top: 2px;
        .element {
          width: 20px;
          height: 20px;
          padding: 0;
          margin: 0 2px 0 0;
        }
      }
    }
    .right-div {
      padding-left: 20px;
      .ass {
        .row {
          display: flex;
          padding-top: 2px;
          .element {
            width: 20px;
            height: 20px;
            padding: 0;
            margin: 0 2px 0 0;
          }
        }
      }
      .score-div {
        div {
          height: 20px;
          line-height: 20px;
        }
        .ztks {
          width: 100px;
          height: 40px;
          margin-bottom: 10px;
          background-color: palevioletred;
          text-align: center;
          line-height: 40px;
        }
      }
    }
  }
  .control {
    width: 220px;
    p {
      width: 220px;
      height: 40px;
      text-align: center;
      line-height: 40px;
      background-color: #B940EF;
      margin-bottom: 20px;
    }
    .control-center {
      align-items: center;
      display: flex;
      justify-content: space-between;
      margin-bottom: 20px;
      div {
        width: 90px;
        height: 40px;
        text-align: center;
        line-height: 40px;
        background-color: #B940EF;
      }
    }
  }
}
</style>

 Tool functions:

//渐变色
export const color = [
  [
    'linear-gradient(180deg, #FFA7EB 0%, #F026A8 100%)',
    'linear-gradient(180deg, #DFA1FF 0%, #9A36F0 100%)',
    'linear-gradient(180deg, #9EAAFF 0%, #3846F4 100%)',
    'linear-gradient(180deg, #7BE7FF 2%, #1E85E2 100%)',
    'linear-gradient(180deg, #89FED8 0%, #18C997 100%)',
    'linear-gradient(180deg, #FFED48 0%, #FD9E16 100%)',
    'linear-gradient(180deg, #FFBA8D 1%, #EB6423 100%)',
  ],
  [
    '#2B7AF5','#2B9DF5','#79CFFF','#1B67DD','#4F94FF','#2180F2','#3FD0FF',
  ],
];

//7种方块元素
export const blockMod = (color) => {
  let a = {
    site: [0, 1, 0, 2, 1, 2, 2, 2],
    color: color[0],
  };
  let b = {
    site: [0, 1, 1, 1, 1, 2, 2, 2],
    color: color[1],
  };
  let c = {
    site: [1, 1, 1, 2, 2, 1, 2, 2],
    color: color[2],
  };
  let d = {
    site: [1, 0, 1, 1, 1, 2, 1, 3],
    color: color[3],
  };
  let e = {
    site: [0, 2, 1, 1, 1, 2, 2, 1],
    color: color[4],
  };
  let f = {
    site: [0, 1, 0, 2, 1, 1, 2, 1],
    color: color[5],
  };
  let g = {
    site: [1, 1, 2, 0, 2, 1, 2, 2],
    color: color[6],
  };
  return ([a, b, c, d, e, f, g]);
};

//旋转规则
export const transition = [
  [
    {
      x: 1, y: 1,
    }, {
      x: 1, y: 0,
    }, {
      x: 0, y: -2,
    }, {
      x: -2, y: 1,
    }
  ],
  [
    {
      x: 1, y: 1,
    }, {
      x: 1, y: 0,
    }, {
      x: 0, y: -2,
    }, {
      x: -2, y: 1,
    }
  ],
  [
    {
      x: 0, y: 1,
    }, {
      x: 1, y: 0,
    }, {
      x: 0, y: -1,
    }, {
      x: -1, y: 0,
    }
  ],
  [
    {
      x: -1, y: 2,
    }, {
      x: 1, y: 1,
    }, {
      x: 2, y: -1,
    }, {
      x: -2, y: -2,
    }
  ],
  [
    {
      x: 2, y: 0,
    }, {
      x: 0, y: -1,
    }, {
      x: -1, y: -1,
    }, {
      x: -1, y: 2,
    }
  ],
  [
    {
      x: 1, y: 1,
    }, {
      x: 1, y: 0,
    }, {
      x: 0, y: -2,
    }, {
      x: -2, y: 1,
    }
  ],
  [
    {
      x: 0, y: 0,
    }, {
      x: 1, y: 0,
    }, {
      x: -1, y: 0,
    }, {
      x: 0, y: 0,
    }
  ],
]

3. Principle analysis

3.1. Interface construction

  The main interface is 20X10, similar to a greedy snake, and the random blocks on the secondary interface are 4x4, both of which are double for loops. Just call gameFrame() during initialization.

3.2. Block creation

  Mainly explain the randomly generated blocks, each of which is composed of 4 small squares, forming 7 basic styles. Rotating in four directions on its own basis is all the possibilities of whereabouts. The reference coordinates are shown on the right:

3.3. Block rotation

  Rotation b: current block d: rendering position z: the rendered object is now the next one xz: current rotation angle, rotate in change (b, d, z, xz), here the transition in the tool function is used, rotation The core is to find a fixed point and see the changes in x and Y coordinates , that is (x=y,y=-x). By adding the blockMod in the tool function, the corresponding falling blocks can be generated in sequence.

3.4. Block movement

  While generating one, we need to consider the generation and random rotation of the next block, so b: block, d: position, n: 0 erase, 1 generated, 2 will definitely fall to the bottom layer, in renderBlock(b, d, n) The shape is rendered in the method.

  We have three directions: moveDown(), moveLeft(), moveRight(). The principle here is basically similar to that of Snake, but there are more factors to consider when moving downward. When rotating, b: current block xz: current The rotation angle change1() method uses deep copy in many places in order to maintain the original shape.

3.5. Movement judgment

  Predict whether it can move or change e: 1 move right, 2 move left, 3 move down, 4 changes, implemented in the canMove(e) method. The main thing is to determine whether the next position is occupied. When you reach the bottom: make sure the position cannot be moved anymore and ensure that other blocks above will not penetrate it when they fall . This is an issue to consider.

3.6. Determination and removal of whereabouts

  Determine whether it can be eliminated: traverse the entire interface, and when a row is full, delete the blocks in that row one by one, and keep the positions of the blocks attached above unchanged, forming a "swallowed" phenomenon. When eliminating a full row, move the upper block down, judge from bottom to top, and then clear the current block to avoid conflicts.

3.7. Score calculation


  The fractions are 100 for one row, 300 for two rows, 600 for three rows, and 1000 for four rows. When one row is eliminated, the number of mixed scores increases. At the same time, as the level increases, the speed will become faster and the difficulty will increase.

Guess you like

Origin blog.csdn.net/qq_44930306/article/details/131132997