<template> <div style="position: relative;"> <canvas :id="id" width="320" height="620" tabindex="0"/> <div style="position: absolute;top: 0;left: 350px;"> <p>{{score}}</p> <el-button @click="reset">reset</el-button> </div> </div> </template> <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; import { v1 as uuid } from "uuid"; import { SCanvasView, SColor, SPainter, STextAlign } from "@persagy-web/draw/lib"; /** * 俄罗斯方块视图 * * @author 郝建龙 <haojianlong@sagacloud.com> */ class TestView extends SCanvasView { /** 背景表示数组 */ map: number[][] = []; /** 方块类型索引 0-6 */ boxType: number = Number(Math.floor(Math.random() * 7)); /** 方块变形索引 0-3 */ dir: number = Number(Math.floor(Math.random() * 4)); /** 所有方块集合 */ box: number[][][] = [ [ [0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], ], [ [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0], ], [ [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], ], [ [0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0], ], [ [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 0, 1, 1], [0, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0], ], [ [0, 0, 0, 0], [0, 1, 1, 0], [0, 0, 1, 1], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 1, 0], [0, 1, 0, 0], ], [ [0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 1, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], ], [ [0, 0, 0, 0], [0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 1, 1], [0, 1, 0, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 0, 0], ], [ [0, 0, 0, 0], [1, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 1, 0], [0, 0, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 1], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 0, 1, 0], [0, 1, 1, 1], [0, 0, 0, 0], ], [ [0, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], ], [ [0, 0, 0, 0], [1, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0], ], [ [0, 0, 1, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0], ] ]; /** 方块初始位置 x 坐标 */ x = 5; /** 方块初始位置 y 坐标 */ y = 0; /** 记录上次刷新时间 */ t = Date.now(); /** 是否游戏结束 */ gameOver: boolean = false; /** 方块颜色 */ boxColor: SColor = new SColor(); /** * 构造函数 * * @param id canvas DOM id */ constructor(id: string) { super(id); this.initMap(); this.initColor(); } // Constructor /** * 键盘按下事件 * * @param event 事件对象 */ onKeyDown(event: KeyboardEvent): void { // 如果游戏结束, 不响应任何键 if (this.gameOver) { return; } if (event.code == "KeyW") { // 用户按下 W 键,变形 // 如果变形不会碰撞 if (!this.checkKnocked(this.x, this.y, (this.dir + 1) % 4)) { // 进行变形操作 this.dir = (this.dir + 1) % 4; } } else if (event.code == "KeyA") { // 用户按下 A 键,左移 // 如果左移不会碰撞 if (!this.checkKnocked(this.x - 1, this.y, this.dir)) { // 左移 this.x--; } } else if (event.code == "KeyD") { // 用户按下 D 键,右移 // 如果右移不会碰撞 if (!this.checkKnocked(this.x + 1, this.y, this.dir)) { // 右移 this.x++; } } else if (event.code == "KeyS") { // 用户按下 S 键,下移 if (!this.checkKnocked(this.x, this.y + 1, this.dir)) { // 下移 this.y++; } else { this.fillMap(); } } // 更新视图 this.update(); } /** * Item 绘制操作 * * @param painter 绘制对象 */ onDraw(painter: SPainter): void { // 如果游戏结束,绘制文字 if (this.gameOver) { painter.brush.color = SColor.Blue; painter.font.size = 20; painter.font.textAlign = STextAlign.Center; painter.drawText("GAME OVER", 180, 30); return; } // 清空画布 painter.clearRect(0, 0, this.width, this.height); painter.pen.color = new SColor("#0bc0ff"); painter.brush.color = new SColor("#2accc7"); this.drawMap(painter); painter.pen.color = this.boxColor; painter.brush.color = this.boxColor; // 绘制图形 this.drawBox(painter); // 判断是否到了 box 下落的时间(用时间间隔判断,不会受机器的性能影响) if (Date.now() - this.t > 500) { if (!this.checkKnocked(this.x, this.y + 1, this.dir)) { // 下移是否碰撞 this.y++; } else { this.fillMap(); } this.t = Date.now(); } // 刷新游戏画面 this.update(); } /** * 初始化背景 */ initMap(): void { this.gameOver = false; 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(-1); } else { m1.push(0); } } this.map.push(m1); } } /** * 初始化方块形状 */ initBox(): void { // box 的初始位置 this.x = 5; this.y = 0; // box 的类型与方向 this.boxType = Number(Math.floor(Math.random() * 7)); this.dir = Number(Math.floor(Math.random() * 4)); this.initColor(); } /** * 初始化方块颜色 */ initColor(): void { const a = Number(Math.floor(Math.random() * 128)); const b = Number(Math.floor(Math.random() * 128)); const c = Number(Math.floor(Math.random() * 128)); this.boxColor = new SColor(a, b, c); } /** * 绘制背景 * * @param painter 绘制对象 */ private drawMap(painter: SPainter): void { // 遍历行 for (let row = 0; row < 22; row++) { // 遍历列 for (let col = 0; col < 14; col++) { this.drawShape(painter, row, col, this.map[row][col]); } } } /** * 绘制实体图形 * * @param painter 绘制对象 * @param row 行 * @param col 列 * @param type 图类型 */ private drawShape(painter: SPainter, row: number, col: number, type: number): void { const x = col * 30 + 11 - 60; const y = row * 30 + 11; switch (type) { // 类型判断 case 0: // 空白 break; case -1: // 边界 painter.drawRoundRect(x, y, 28, 28, 6); break; default: // 填充 painter.drawRoundRect(x, y, 28, 28, 6); break; } } /** * 绘制方块 * * @param painter 绘制对象 */ private drawBox(painter: SPainter): void { for (let row = 0; row < 4; row++) { // 绘制图形行数 for (let col = 0; col < 4; col++) { // 绘制图形列数 this.drawShape(painter, row + this.y, col + this.x, this.box[this.boxType * 4 + this.dir][row][col]); } } } /** * 是否碰撞 * * @param x x 坐标 * @param y y 坐标 * @param dir 方块变形索引 * @return 是否碰撞 */ private checkKnocked(x: number, y: number, dir: number): boolean { for (let row = 0; row < 4; row++) { // 绘制图形行数 for (let col = 0; col < 4; col++) { // 绘制图形列数 // 判断是否重合,每种图形四个一组,四种变形的某种变形,不为零表示绘制(确定方块),坐标转换,背景图中绘制 if (this.box[this.boxType * 4 + dir][row][col] != 0 && this.map[y + row][x + col] != 0) { return true; } } } // 返回不发生碰撞 return false; } /** * 将方块合到背景中 */ private fillMap(): void { for (let row = 0; row < 4; row++) { // 遍历图形的行数 for (let col = 0; col < 4; col++) { // 遍历图形的列数 if (this.box[this.boxType * 4 + this.dir][row][col] == 1) { // 判断图形绘制 // 填充至背景中 this.map[this.y + row][this.x + col] = 1; } } } this.initBox(); if (this.checkKnocked(this.x, this.y, this.dir)) { // 判断背景上的图形与初始化图形是否重合 // 游戏结束 this.gameOver = true; } this.remove(); } /** * 消除满行 */ private remove(): void { // 消除行数 let removeCount = 0; // 循环行数,不包含底部填充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; break; } } if (flag) { // 标记删除 this.map.splice(row, 1); // 顶部加一行空行 this.map.unshift(this.randomRow(10, 0)); // 统计消除的行数 removeCount++; } } if (removeCount > 0) { // 记录消除行数 this.$emit("score", removeCount); // 删除行 this.map.shift(); // 底部随机产生一行 this.map.splice(19, 0, this.randomRow()); } } /** * 随机数组生成 * * @return 数组 */ private randomRow(len: number = 10, num: number = 2): number[] { let array = new Array(len + 4).fill(-1); // 生成随机数,循环不包含左右填充格 for (let i = 2; i < array.length - 2; i++) { array[i] = Math.floor(num * Math.random()); } return array; } } @Component export default class ElsfkCanvas extends Vue { /** 图 id */ id: string = uuid(); /** 记分数组列表 */ scoreList: number[] = [100, 300, 600, 1000]; /** 记分分数 */ score: number = 0; /** 实例化 view */ view: TestView | undefined = undefined; /** * 页面挂载 */ mounted(): void { this.view = new TestView(this.id); this.view.connect("score", this, this.changeScore); document.getElementById(this.id)!!.focus(); } /** * 分值展示 * * @param v canvas 视图对象 * @param count 计数分值 */ changeScore(v: SCanvasView, count: number[]) { //根据消除行数判断分值 this.score += this.scoreList[count[0] - 1]; } /** * 重置操作 */ reset() { this.view!!.initMap(); this.view!!.initBox(); this.score = 0; this.view!!.update(); document.getElementById(this.id)!!.focus(); } } </script>