ソースを参照

fix> 修改俄罗斯方块示例代码。

sybotan 4 年 前
コミット
0250f25b88
1 ファイル変更287 行追加148 行削除
  1. 287 148
      docs/.vuepress/components/engine/demo/elsfk.vue

+ 287 - 148
docs/.vuepress/components/engine/demo/elsfk.vue

@@ -1,28 +1,50 @@
 <template>
-    <div>
-        <canvas :id="id" width="320" height="620" tabindex="0"/>
+    <div style="position: relative;">
+        <canvas id="tetris1" width="320" height="620" style="border: 1px #ccc solid" tabindex="0"/>
+        <div style="position: absolute;top: 0;left: 350px;">
+            <p>{{view?view.score:0}}</p>
+            <button @click="reset">重新开始</button>
+        </div>
     </div>
 </template>
 
 <script lang="ts">
 import { Component, Vue } from "vue-property-decorator";
-import { v1 as uuid } from "uuid";
-import { SCanvasView, SColor, SPainter } from "@persagy-web/draw/lib";
+import { SColor } from "sybotan-base";
+import { SCanvas, SCanvasView, SPaint } from "sybotan-graph";
+import { SPaintStyle } from "sybotan-graph/lib";
 
 /**
- * 俄罗斯方块视图
+ * 俄罗斯广场视图
  *
- * @author 郝洁 <haojie@persagy.com>
+ * @author  庞利祥 <sybotan@126.com>
  */
-class TestView extends SCanvasView {
-    /** 背景表示数组 */
-    map: number[][] = [];
-    /** 方块类型索引 */
-    fkType: number = Number(Math.floor(Math.random() * 7));  // 0-6
-    /** 方块变形索引 */
-    dir: number = Number(Math.floor((Math.random() * 4)));  // 0-3
+class TetrisView extends SCanvasView {
+    /** 小格画笔 */
+    gridPaint = new SPaint();
+    /** 文字画笔 */
+    textPaint = new SPaint();
+    /** 网格数组 (二维数组初始化) */
+    gridMap = Array(20)
+        .fill(null)
+        .map(() => Array(10).fill(0));
+    /** 方块类型索引 0-6 */
+    boxType = Number(Math.floor(Math.random() * 7));
+    /** 方块变形索引 0-3 */
+    dir = Number(Math.floor(Math.random() * 4));
+    /** 下落方块行坐标 */
+    currRow = 0;
+    /** 下落方块列坐标 */
+    currCol = 3;
+    /** 记录上次刷新时间 */
+    lastTime = Date.now();
+    /** 记分分数 */
+    score = 0;
+    /** 是否游戏结束 */
+    isGameOver = false;
+
     /** 所有方块集合 */
-    fk: number[][][] = [
+    box: number[][][]=[
         [
             [0, 0, 0, 0],
             [0, 1, 1, 0],
@@ -192,224 +214,341 @@ class TestView extends SCanvasView {
             [0, 0, 0, 0],
         ]
     ];
-    /** 方块初始位置x坐标 */
-    x = 5;
-    /** 方块初始位置y坐标 */
-    y = 0;
-    /** 记录上次刷新时间 */
-    t = Date.now();
+
+    // /** 记分数组列表 */
+    private _scoreList = [100, 300, 600, 1000];
 
     /**
      * 构造函数
+     *
      * @param id canvas DOM id
      */
     constructor(id: string) {
         super(id);
-        this.initMap();
+
+        // 设置绘制网格的颜色
+        this.gridPaint.fill = SColor.Red;
+        this.gridPaint.style = SPaintStyle.Fill;
+
+        this.textPaint.fill = SColor.Blue;
+        this.textPaint.font.size = 40;
+
+        // 初始化游戏
+        this.init();
+    }
+
+    /**
+     * 初始化游戏
+     */
+    init(): void {
+        // 遍历网格的行
+        for (let r = 0; r < 20; r++) {
+            // 遍历网格的列
+            for (let c = 0; c < 10; c++) {
+                // 将网格所有数据填充为 0
+                this.gridMap[r][c] = 0;
+            }
+        }
+
+        // 生成新的下落box
+        this.newFallBox();
+
+        // 初始化分数
+        this.score = 0;
+        // 游戏结束标记为 false
+        this.isGameOver = false;
     }
 
     /**
      * 键盘按下事件
-     * @param event 事件对象
+     *
+     * @param   event   事件对象
      */
     onKeyDown(event: KeyboardEvent): void {
-        if (event.code == "KeyW") { // 按键 w
-            if (!this.isPz(this.x, this.y, (this.dir + 1) % 4)) { // 检查变形后是否碰撞
+        // 如果游戏结束, 不响应任何键
+        if (this.isGameOver) {
+            return;
+        }
+
+        if (event.code == "KeyW") {     // 用户按下 W 键,变形
+            // 如果变形不会碰撞
+            if (!this.checkCollide(this.boxType, (this.dir + 1) % 4, this.currRow, this.currCol)) {
+                // 进行变形操作
                 this.dir = (this.dir + 1) % 4;
             }
-        } else if (event.code == "KeyA") {  // 按键 a
-            if (!this.isPz(this.x - 1, this.y, this.dir)) { // 检查左移后是否碰撞
-                this.x--;
-            }
-        } else if (event.code == "KeyD") { // 按键 d
-            if (!this.isPz(this.x + 1, this.y, this.dir)) { // 检查右移后是否碰撞
-                this.x++;
+        } else if (event.code == "KeyA") {  // 用户按下 A 键,左移
+            // 如果左移不会碰撞
+            if (!this.checkCollide(this.boxType,this.dir, this.currRow, this.currCol - 1)) {
+                // 左移
+                this.currCol--;
             }
-        } else if (event.code == "KeyS") { // 按键 s
-            if (!this.isPz(this.x, this.y + 1, this.dir)) { // 检查下移后是否碰撞
-                this.y++;
-            } else {
-                this.fullMap();
-                this.xc();
+        } else if (event.code == "KeyD") {  // 用户按下 D 键,右移
+            // 如果右移不会碰撞
+            if (!this.checkCollide(this.boxType,this.dir, this.currRow, this.currCol + 1)) {
+                // 右移
+                this.currCol++;
             }
+        } else if (event.code == "KeyS") {    // 用户按下 S 键,下移
+            // box 下落
+            this.fallBox();
         }
 
+        // 更新视图
         this.update();
     }
 
     /**
-     * Item 绘制操作
+     * 绘制游戏画面
      *
-     * @param painter   绘制对象
+     * @param   canvas      画布
      */
-    onDraw(painter: SPainter): void {
-        //清除画布
-        painter.clearRect(0, 0, this.width, this.height);
-        //绘制操作相关命令
-        painter.pen.color = SColor.Transparent;
-        painter.brush.color = SColor.Red;
-
-        this.drawMap(painter);
-        this.drawFk(painter);
-
-        if (Date.now() - this.t > 500) {  // 下落速度,下移一格
-            if (!this.isPz(this.x, this.y + 1, this.dir)) {  // 下移是否碰撞
-                this.y++;
-            } else {
-                this.fullMap();
-                this.xc();
+    onDraw(canvas: SCanvas): void {
+        // 清空画布
+        this.clear();
+
+        // 绘制网格
+        this.drawGridMap(canvas);
+
+        // 如果游戏结束
+        if (this.isGameOver) {
+            // 闪烁显示 “Game Over”
+            if (Date.now() % 1000 > 500) {
+                canvas.drawText("Game Over", 50, 300, this.textPaint);
             }
 
-            this.t = Date.now();
+            // 刷新游戏画面
+            this.update();
+
+            return;
+        }
+
+        // 绘制下落的广场
+        this.drawFallBox(canvas);
+
+        // 判断是否到了 box 下落的时间(用时间间隔判断,不会受机器的性能影响)
+        if (Date.now() - this.lastTime > 500) {
+            // box 下落
+            this.fallBox();
+            this.lastTime = Date.now();
         }
+
+        // 刷新游戏画面
         this.update();
     }
 
     /**
-     *初始化背景
+     * 绘制网格
+     *
+     * @param   canvas      画布
      */
-    private initMap(): void {
-        this.map = [];
-        for (let row = 0; row < 22; row++) {   // 循环行数
-            const m1: number[] = []
-            for (let col = 0; col < 14; col++) {  // 循环列数
-                if (row > 19 || col < 2 || col > 11) {  // 左侧,右侧,底部补充两个格
-                    // -1 代表左右填充
-                    m1.push(2);
-                } else {
-                    m1.push(0);
-                }
+    private drawGridMap(canvas: SCanvas): void {
+        // 遍历行
+        for (let r = 0; r < 20; r++) {
+            // 遍历列
+            for (let c = 0; c < 10; c++) {
+                // 绘制小格
+                this.drawGird(canvas, this.gridMap[r][c], r, c);
             }
-            this.map.push(m1);
         }
     }
 
     /**
-     * 绘制背景
+     * 绘制下落的 Box
      *
-     * @param painter 绘制对象
+     * @param   canvas      画布
      */
-    private drawMap(painter: SPainter): void {
-        for (let row = 0; row < 22; row++) { // 行数
-            for (let col = 0; col < 14; col++) {  // 列数
-                const x = col * 30 + 11 - 60;
-                const y = row * 30 + 11;
-                if (this.map[row][col] == 1) { //赋到背景
-                    painter.drawRect(col * 30 + 11 - 60, row * 30 + 11, 28, 28);
-                }
-
-                if (this.map[row][col] == 2) { //底加一行
-                    painter.drawRect(col * 30 + 11 - 60, row * 30 + 11, 28, 28);
-                }
+    private drawFallBox(canvas: SCanvas): void {
+        // 遍历 box 的行
+        for (let r = 0; r < 4; r++) {
+            // 遍历 box 的列
+            for (let c = 0; c < 4; c++) {
+                // 绘制小格
+                this.drawGird(canvas, this.box[this.boxType * 4 + this.dir][r][c], this.currRow + r, this.currCol + c);
             }
         }
     }
 
     /**
-     * 绘制实体图形
+     * 执行 box 下落流程
+     */
+    private fallBox(): void {
+        // 如果可以下移
+        if (!this.checkCollide(this.boxType,this.dir, this.currRow + 1, this.currCol)) {
+            // 下移并刷新游戏画面
+            this.currRow++;
+            this.update();
+            return;
+        }
+
+        // 将 box 中的内容填充到网格
+        this.boxIntoMap();
+
+        // 如果 box 没有下移过(游戏结束条件)
+        if (this.currRow <= -1) {
+            // 游戏结束,并更新游戏画面
+            this.isGameOver = true;
+            this.update();
+            return;
+        }
+
+        // 移除满行
+        const rows = this.removeFullRow();
+        // 如果移除大于 1 行
+        if (rows > 0) {
+            this.score += this._scoreList[rows - 1];
+        }
+        // 生成新的下落 box
+        this.newFallBox();
+    }
+
+    /**
+     * 画一个小格
      *
-     * @param painter   绘制对象
+     * @param   canvas      画布
+     * @param   type        小格的图类型
+     * @param   row         行
+     * @param   col         列
      */
-    private drawFk(painter: SPainter): void {
-        for (let row = 0; row < 4; row++) { // 绘制图形行数
-            for (let col = 0; col < 4; col++) { // 绘制图形列数
-                if (this.fk[this.fkType * 4 + this.dir][row][col] == 1) {
-                    painter.drawRect((col + this.x) * 30 + 11 - 60, (row + this.y) * 30 + 11, 28, 28);
-                }
-            }
+    private drawGird(canvas: SCanvas, type: number, row: number, col: number): void {
+        // 如果类型不为 0
+        if (type != 0) {
+            // 绘制小格
+            canvas.drawRect(col * 30 + 11, row * 30 + 11, 28, 28, this.gridPaint);
         }
     }
 
     /**
-     * 是否碰撞
+     * 判断当前下落的方块是否会发生碰撞
      *
-     * @param x     x坐标
-     * @param y     y坐标
-     * @param dir   方块变形索引
-     * @return 是否碰撞
+     * @param   type        下落广场的类型
+     * @param   dir         方块的朝向
+     * @param   row         下落方块的行坐标
+     * @param   col         下落广场的列坐标
+     * @return  碰撞返回 true,否则返回 false 。
      */
-    private isPz(x: number, y: number, dir: number): boolean {
-        for (let row = 0; row < 4; row++) { // 绘制图形行数
-            for (let col = 0; col < 4; col++) {  // 绘制图形列数
-                // 判断是否重合,每种图形四个一组,四种变形的某种变形,不为零表示绘制(确定方块),坐标转换,背景图中绘制
-                if (this.fk[this.fkType * 4 + dir][row][col] == 1 && this.map[y + row][x + col] != 0) {
-                    return true;
+    private checkCollide(type: number, dir: number, row: number, col: number): boolean {
+        // 遍历 box 的行
+        for (let r = 0; r < 4; r++) {
+            const r1 = row + r;
+            // 遍历 box 的列
+            for (let c = 0; c < 4; c++) {
+                const c1 = col + c;
+                if (this.box[this.boxType * 4 + dir][r][c] != 0) {
+                    // 如果没有对应的网络格(下标越界),则为碰撞
+                    if (c1 < 0 || c1 >= 10 || r1 >= 20) {
+                        return true;
+                    }
+
+                    // 如果对应的网格不为 0,则为碰撞
+                    if (r1 > 0 && this.gridMap[row + r][col + c] != 0) {
+                        return true;
+                    }
                 }
             }
         }
+
+        // 返回不发生碰撞
         return false;
     }
 
     /**
-     * 将方块合到背景中
+     * 当前下落 box 填充到网格
      */
-    private fullMap() {
-        for (let row = 0; row < 4; row++) { // 遍历图形的行数
-            for (let col = 0; col < 4; col++) { // 遍历图形的列数
-                if (this.fk[this.fkType * 4 + this.dir][row][col] == 1) {
-                    this.map[this.y + row][this.x + col] = 1;
+    private boxIntoMap() {
+        // 遍历 box 的行
+        for (let r = 0; r < 4; r++) {
+            const r1 = this.currRow + r;
+            // 遍历 box 的列
+            for (let c = 0; c < 4; c++) {
+                const g = this.box[this.boxType * 4 + this.dir][r][c]
+                const c1 = this.currCol + c;
+                // 如果下落的 box 对应的格不为空
+                if (g != 0) {
+                    // 如果没有对应的网格(下标越界),则处理下一格
+                    if (c1 < 0 || c1 >= 10 || r1 < 0 || r1 >= 20) {
+                        continue;
+                    }
+                    // 将 box 填入到网格的对应位置
+                    this.gridMap[r1][c1] = g;
                 }
             }
         }
+    }
+
+    /**
+     * 生成新的下落方块
+     */
+    private newFallBox() {
+        // box 的初始位置
+        this.currCol = 3;
+        this.currRow = -3;
 
-        this.x = 5;
-        this.y = 0;
-        this.fkType = Number((Math.random() * 6).toFixed());
-        this.dir = Number((Math.random() * 2).toFixed());
+        // box 的类型与方向
+        this.boxType = Number(Math.floor(Math.random() * 7));
+        this.dir = Number(Math.floor(Math.random() * 4));
     }
 
     /**
-     * 消除满行
+     * 消除满行(即不存在空格的行)
+     *
+     * @return  消除的行数
      */
-    private xc(): void {
-        // 循环行数,不包含底部填充2行
-        for (let row = 0; row < 20; row++) {
-            // 消除标记,true 是
-            let flag = true;
-            // 若当前行有一个空格,则不消除
-            for (let col = 2; col < 12; col++) {
-                if (this.map[row][col] == 0) {  // 存在空格
-                    flag = false;
+    private removeFullRow(): number {
+        // 用于统计消除的行数
+        let count = 0;
+
+        // 遍历行
+        for (let r = 0; r < 20; r++) {
+            // 消除标记
+            let isRemove = true;
+            // 遍历列
+            for (let c = 0; c < 10; c++) {
+                // 有任意一格为空,则标识消除标记为 false,跳出循环
+                if (this.gridMap[r][c] == 0) {
+                    isRemove = false
                     break;
                 }
             }
 
-            if (flag) { // 标记删除
-                this.map.splice(row, 1);
-                this.map.unshift()
-                // 顶部加一行空行
-                this.map.unshift(this.randomRow(10, 0))
-                // this.map.unshift([2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2]);
-                // row--;
+            // 如果消除标记为 true
+            if (isRemove) {
+                // 清除 row 行。
+                this.gridMap.splice(r,1);
+                this.gridMap.unshift(Array(10).fill(0));
+                // 统计消除的行数
+                count++;
             }
         }
-    }
 
-    /**
-     * 随机数组生成
-     *
-     * @return 数组
-     */
-    private randomRow(len: number = 10, num: number = 2): number[] {
-        let array = new Array(len + 4).fill(2);
-        // 生成随机数,循环不包含左右填充格
-        for (let i = 2; i < array.length - 2; i++) {
-            array[i] = Math.floor(num * Math.random())
-        }
-        return array
+        // 返回消除的行数
+        return count
     }
 }
 
+/**
+ * 俄罗斯方块 demo
+ *
+ * @author  庞利祥 <sybotan@126.com>
+ */
 @Component
-export default class ElsfkCanvas extends Vue {
-    /** 图 id */
-    id: string = uuid();
+export default class Tetris1 extends Vue {
+    /** 实例化 view */
+    view: TetrisView | null = null;
 
     /**
-     * 页面挂载
+     * 页面挂载完成
      */
     mounted(): void {
-        new TestView(this.id);
+        this.view = new TetrisView("tetris1");
+        document.getElementById("tetris1")!!.focus();
+    }
+
+    /**
+     * 重置操作
+     */
+    reset(): void {
+        // 初始化游戏
+        this.view!!.init();
     }
 }
 </script>