|
@@ -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>
|