|
@@ -1,25 +1,88 @@
|
|
|
<template>
|
|
|
- <div>
|
|
|
- <canvas id="canvas" width="400" height="400"></canvas>
|
|
|
- <el-button @click="reset" type="primary">重新开始</el-button>
|
|
|
+ <div style="position: relative;">
|
|
|
+ <canvas id="canvas" width="400" height="400" style="border: 1px #ccc solid;background: #bbada0" tabindex="0"/>
|
|
|
+ <div style="position: absolute;top: 0;left: 450px;">
|
|
|
+ <p>分数:{{ view ? view.score : 0 }}</p>
|
|
|
+ <el-button @click="reset" type="primary">重新开始</el-button>
|
|
|
+ <p>W:上移</p>
|
|
|
+ <p>S:下移</p>
|
|
|
+ <p>A:左移</p>
|
|
|
+ <p>D:右移</p>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts">
|
|
|
-import { Component, Vue } from "vue-property-decorator"
|
|
|
-import { SCanvasView, SColor, SPainter, STextAlign } from "@persagy-web/draw/lib";
|
|
|
+import {Component, Vue} from "vue-property-decorator"
|
|
|
+import {SCanvasView, SColor, SPainter, STextAlign, STextBaseLine} from "@persagy-web/draw/lib";
|
|
|
|
|
|
/**
|
|
|
- * 2048
|
|
|
+ * 游戏 2048
|
|
|
*
|
|
|
* @author 郝洁 <haojie@persagy.com>
|
|
|
*/
|
|
|
class GameView extends SCanvasView {
|
|
|
- /** 游戏结束 */
|
|
|
+ /** 网格数组初始化 */
|
|
|
+ gridMap = Array(4).fill(null).map(() => Array(4).fill(0))
|
|
|
+ /** 游戏结束标识 */
|
|
|
gameOver: boolean = false
|
|
|
- /** 格子宽度 */
|
|
|
- box_width: number = this.width * 0.8 * 0.25;
|
|
|
- margin_width: number = this.width * 0.2 * 0.20;
|
|
|
+ /** 游戏累计分数 */
|
|
|
+ score = 0;
|
|
|
+ /** 数字画笔 */
|
|
|
+ // numPaint = new SPainter();
|
|
|
+ /** 文字画笔 */
|
|
|
+ // textPaint = new SPainter();
|
|
|
+ /** 格子顏色 */
|
|
|
+ color = {
|
|
|
+ 0: {
|
|
|
+ font: "#ffffffff",
|
|
|
+ background: "#eee4da59"
|
|
|
+ },
|
|
|
+ 2: {
|
|
|
+ font: "#776e65",
|
|
|
+ background: "#eee4da"
|
|
|
+ },
|
|
|
+ 4: {
|
|
|
+ font: "#776e65",
|
|
|
+ background: "#ede0c8"
|
|
|
+ },
|
|
|
+ 8: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#f2b179"
|
|
|
+ },
|
|
|
+ 16: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#f59563"
|
|
|
+ },
|
|
|
+ 32: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#f67c5f"
|
|
|
+ },
|
|
|
+ 64: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#f65e3b"
|
|
|
+ },
|
|
|
+ 128: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#edcf72"
|
|
|
+ },
|
|
|
+ 256: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#edcc61"
|
|
|
+ },
|
|
|
+ 512: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#edc850"
|
|
|
+ },
|
|
|
+ 1024: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#edc53f"
|
|
|
+ },
|
|
|
+ 2048: {
|
|
|
+ font: "#f9f6f2",
|
|
|
+ background: "#edc22e"
|
|
|
+ },
|
|
|
+ };
|
|
|
|
|
|
/**
|
|
|
* 构造函数
|
|
@@ -28,6 +91,7 @@ class GameView extends SCanvasView {
|
|
|
*/
|
|
|
constructor(id: string) {
|
|
|
super(id);
|
|
|
+ // 初始化游戏
|
|
|
this.init()
|
|
|
}
|
|
|
|
|
@@ -35,16 +99,32 @@ class GameView extends SCanvasView {
|
|
|
* 初始化游戏
|
|
|
*/
|
|
|
init(): void {
|
|
|
+ // 遍历网格的行
|
|
|
+ for (let r = 0; r < 4; r++) {
|
|
|
+ // 遍历网格的列
|
|
|
+ for (let c = 0; c < 4; c++) {
|
|
|
+ this.gridMap[r][c] = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 随机填充两个 2
|
|
|
+ this.gridMap[Math.floor(Math.random() * 4)][Math.floor(Math.random() * 4)] = 2
|
|
|
+ this.gridMap[Math.floor(Math.random() * 4)][Math.floor(Math.random() * 4)] = 2
|
|
|
+
|
|
|
+ // 初始化游戏分数
|
|
|
+ this.score = 0
|
|
|
+
|
|
|
+ // 游戏结束标识标记
|
|
|
+ this.gameOver = false
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 键盘按下事件
|
|
|
*
|
|
|
- * @param event 事件对象
|
|
|
+ * @param event 事件对象
|
|
|
*/
|
|
|
onKeyDown(event: KeyboardEvent): void {
|
|
|
- if ( /* 游戏结束*/ 1) {
|
|
|
+ if (this.gameOver) {
|
|
|
return;
|
|
|
}
|
|
|
// 按键判断
|
|
@@ -52,94 +132,429 @@ class GameView extends SCanvasView {
|
|
|
case "KeyW": // 上移动作
|
|
|
case "ArrowUp":
|
|
|
// 判断游戏是否继续,底部产生随机位置随机数
|
|
|
+ this.moveUp();
|
|
|
break;
|
|
|
|
|
|
case "KeyA": // 左移动作
|
|
|
case "ArrowLeft":
|
|
|
// 判断游戏是否继续,右部产生随机位置随机数
|
|
|
+ this.moveLeft();
|
|
|
break;
|
|
|
|
|
|
case "KeyD": // 右移动作
|
|
|
case "ArrowRight":
|
|
|
+ this.moveRight();
|
|
|
// 判断游戏是否继续,左部产生随机位置随机数
|
|
|
break;
|
|
|
// 按键 下
|
|
|
case "KeyS": // 下移动作
|
|
|
case "ArrowDown":
|
|
|
// 判断游戏是否继续,上部产生随机位置随机数
|
|
|
+ this.moveDown();
|
|
|
break;
|
|
|
default :
|
|
|
// do nothing
|
|
|
}
|
|
|
-
|
|
|
// 更新视图
|
|
|
+ this.update();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 生成随机的格子
|
|
|
+ * 上移
|
|
|
*/
|
|
|
- generateOneNumber(): void {
|
|
|
- if ( /* 游戏结束*/ 1) {
|
|
|
- return;
|
|
|
+ moveUp(): void {
|
|
|
+ // 数字是否移动
|
|
|
+ let moved = false
|
|
|
+ // 遍历所有的列
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 进行合并操作的列表
|
|
|
+ const numArr = Array(4)
|
|
|
+ // 依次从网格中取要合并的元素
|
|
|
+ for (let j = 0; j < 4; j++) {
|
|
|
+ // 取数
|
|
|
+ numArr[j] = this.gridMap[j][i]
|
|
|
+ }
|
|
|
+ // 移动与合并
|
|
|
+ if (this.moveNum(numArr)) {
|
|
|
+ moved = true;
|
|
|
+ // 合并后的下列表反填入网格
|
|
|
+ for (let j = 0; j < 4; j++) {
|
|
|
+ // 反填
|
|
|
+ this.gridMap[j][i] = numArr[j]
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- //随机一个位置,取新方块出现的位置
|
|
|
+ // 如果移动了数字
|
|
|
+ if (moved) {
|
|
|
+ // 索引列表
|
|
|
+ const indexArr = []
|
|
|
+ // 依次从网格取空格元素的下标索引
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 如果为空格
|
|
|
+ if (this.gridMap[3][i] == 0) {
|
|
|
+ // 保存下标
|
|
|
+ indexArr.push(i)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 生成新的数的下标
|
|
|
+ const index = this.newIndex(indexArr);
|
|
|
+ if (index >= 0) {
|
|
|
+ this.gridMap[3][index] = this.newNum()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 测试是否游戏结束
|
|
|
+ this.gameOver = this.checkGameover()
|
|
|
+ }
|
|
|
|
|
|
- //随机一个数字 2 或 4 ,在随机位置显示随机数字
|
|
|
+ /**
|
|
|
+ * 下移
|
|
|
+ */
|
|
|
+ moveDown(): void {
|
|
|
+ // 是否移动了数字
|
|
|
+ let moved = false;
|
|
|
|
|
|
- return;
|
|
|
+ // 遍历所有列
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 进行合并操作的列表
|
|
|
+ const numArr = Array(4);
|
|
|
+ // 依次从网格中取要合并的元素
|
|
|
+ for (let j = 0; j < 4; j++) {
|
|
|
+ // 取数
|
|
|
+ numArr[j] = this.gridMap[3 - j][i];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移动与合并
|
|
|
+ if (this.moveNum(numArr)) {
|
|
|
+ moved = true;
|
|
|
+ // 合并后的下列表反填入网格
|
|
|
+ for (let j = 0; j < 4; j++) {
|
|
|
+ // 反填
|
|
|
+ this.gridMap[3 - j][i] = numArr[j];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果移动了数字
|
|
|
+ if (moved) {
|
|
|
+ // 索引列表
|
|
|
+ const indexArr = [];
|
|
|
+ // 依次从网格取空格元素的下标索引
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 如果为空格
|
|
|
+ if (this.gridMap[0][i] == 0) {
|
|
|
+ // 保存下标
|
|
|
+ indexArr.push(i)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新生成的数的下标
|
|
|
+ const index = this.newIndex(indexArr);
|
|
|
+ if (index >= 0) {
|
|
|
+ this.gridMap[0][index] = this.newNum();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 测试是否游戏结束
|
|
|
+ this.gameOver = this.checkGameover();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 新生成的数的位置,位置随机产生,位置不可被占用,不可重叠
|
|
|
+ * 左移
|
|
|
+ */
|
|
|
+ moveLeft(): void {
|
|
|
+ // 是否移动了数字
|
|
|
+ let moved = false;
|
|
|
+
|
|
|
+ // 遍历所有行
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 进行合并操作的列表
|
|
|
+ const numArr = Array(4);
|
|
|
+ // 依次从网格中取要合并的元素
|
|
|
+ for (let j = 0; j < 4; j++) {
|
|
|
+ // 取数
|
|
|
+ numArr[j] = this.gridMap[i][j];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移动与合并
|
|
|
+ if (this.moveNum(numArr)) {
|
|
|
+ moved = true;
|
|
|
+ // 合并后的下列表反填入网格
|
|
|
+ for (let j = 0; j < 4; j++) {
|
|
|
+ // 反填
|
|
|
+ this.gridMap[i][j] = numArr[j];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果移动了数字
|
|
|
+ if (moved) {
|
|
|
+ // 索引列表
|
|
|
+ const indexArr = [];
|
|
|
+ // 依次从网格取空格元素的下标索引
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 如果为空格
|
|
|
+ if (this.gridMap[i][3] == 0) {
|
|
|
+ // 保存下标
|
|
|
+ indexArr.push(i)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新生成的数的下标
|
|
|
+ const index = this.newIndex(indexArr);
|
|
|
+ if (index >= 0) {
|
|
|
+ this.gridMap[index][3] = this.newNum();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 测试是否游戏结束
|
|
|
+ this.gameOver = this.checkGameover();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 右移移
|
|
|
+ */
|
|
|
+ moveRight(): void {
|
|
|
+ // 是否移动了数字
|
|
|
+ let moved = false;
|
|
|
+
|
|
|
+ // 遍历所有行
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 进行合并操作的列表
|
|
|
+ const numArr = Array(4);
|
|
|
+ // 依次从网格中取要合并的元素
|
|
|
+ for (let j = 0; j < 4; j++) {
|
|
|
+ // 取数
|
|
|
+ numArr[j] = this.gridMap[i][3 - j];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移动与合并
|
|
|
+ if (this.moveNum(numArr)) {
|
|
|
+ moved = true;
|
|
|
+ // 合并后的下列表反填入网格
|
|
|
+ for (let j = 0; j < 4; j++) {
|
|
|
+ // 反填
|
|
|
+ this.gridMap[i][3 - j] = numArr[j];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果移动了数字
|
|
|
+ if (moved) {
|
|
|
+ // 索引列表
|
|
|
+ const indexArr = [];
|
|
|
+ // 依次从网格取空格元素的下标索引
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 如果为空格
|
|
|
+ if (this.gridMap[i][0] == 0) {
|
|
|
+ // 保存下标
|
|
|
+ indexArr.push(i)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 新生成的数的下标
|
|
|
+ const index = this.newIndex(indexArr);
|
|
|
+ if (index >= 0) {
|
|
|
+ this.gridMap[index][0] = this.newNum();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 测试是否游戏结束
|
|
|
+ this.gameOver = this.checkGameover();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 移动或合并数字,数据相同则合并
|
|
|
*
|
|
|
- * @param indexArray 小格子对应的索引
|
|
|
+ * @param numArr 长度为 4 的数组
|
|
|
+ * @return 移动数字返回 true,否则返回 false
|
|
|
*/
|
|
|
- newLocationRandomNumber(indexArray: []) {
|
|
|
+ private moveNum(numArr: number[]): boolean {
|
|
|
+ // 记录所有合并数字的和
|
|
|
+ let moved = false;
|
|
|
|
|
|
+ // 从第二格开始取数
|
|
|
+ for (let i = 1; i < numArr.length; i++) {
|
|
|
+ const num = numArr[i];
|
|
|
+ // 如果选定的格为0,则取下一格
|
|
|
+ if (num == 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将除去的数往前移
|
|
|
+ for (let j = i = 1; j >= 0; j--) {
|
|
|
+ if (numArr[j] == 0) {
|
|
|
+ // 前移
|
|
|
+ numArr[j] = num;
|
|
|
+ numArr[j + 1] = 0;
|
|
|
+ moved = true
|
|
|
+ } else if (numArr[j] == num) {
|
|
|
+ // 合并
|
|
|
+ numArr[j] += num;
|
|
|
+ numArr[j + 1] = 0;
|
|
|
+ this.score += num * 2;
|
|
|
+ moved = true;
|
|
|
+ break;
|
|
|
+ } else {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 返回所有合并的数组的和
|
|
|
+ return moved
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 新生成数字的下标
|
|
|
+ *
|
|
|
+ * @param indexArr 空格的下标列表的长度大于 0,返回随机的下标;否则返回 0。
|
|
|
+ */
|
|
|
+ private newIndex(indexArr: number[]): number {
|
|
|
+ return indexArr.length > 0 ? indexArr[Math.floor(Math.random() * indexArr.length)] : -1
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 返回新生成的数(2 或 4)
|
|
|
+ *
|
|
|
+ * @return 新生成的数(2 或 4)
|
|
|
+ */
|
|
|
+ private newNum(): number {
|
|
|
+ // 30% 的概率为 4,70% 概率为 2。调整比例可增加或减小游戏的难度。
|
|
|
+ return Math.random() > 0.3 ? 4 : 2
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查是否游戏结束。(没有为空的格,没有相邻两格的数字相同)
|
|
|
+ *
|
|
|
+ * @return 如果结束返回 true,否则返回 false;
|
|
|
+ */
|
|
|
+ private checkGameover(): boolean {
|
|
|
+ // 依次遍历行与列
|
|
|
+ for (let i = 0; i < 4; i++) {
|
|
|
+ // 依次取相邻(横向/纵向)的两个格的数字
|
|
|
+ for (let j = 0; j < 3; j++) {
|
|
|
+ // 判断相邻的格是否相同,或是否为空。
|
|
|
+ if (this.gridMap[i][j] == this.gridMap[i][j + 1] || this.gridMap[i][j] == 0 || this.gridMap[i][j + 1] == 0
|
|
|
+ || this.gridMap[j][i] == this.gridMap[j + 1][i] || this.gridMap[j][i] == 0 || this.gridMap[j + 1][i] == 0) {
|
|
|
+ // 返回游戏未结束
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 返回游戏结束
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
* 绘制游戏画面
|
|
|
*
|
|
|
- * @param canvas 画布
|
|
|
+ * @param canvas 绘制对象
|
|
|
*/
|
|
|
onDraw(canvas: SPainter): void {
|
|
|
canvas.clearRect(0, 0, this.width, this.height)
|
|
|
- //绘制格子
|
|
|
+ canvas.pen.color = SColor.Transparent;
|
|
|
+ this.drawMap(canvas);
|
|
|
+ this.drawNum(canvas);
|
|
|
+
|
|
|
+ //如果游戏结束,绘制格子
|
|
|
if (this.gameOver) {
|
|
|
canvas.brush.color = SColor.Red;
|
|
|
canvas.font.size = 40;
|
|
|
canvas.font.textAlign = STextAlign.Center;
|
|
|
- canvas.drawText("Game Over");
|
|
|
+ if (Date.now() % 100 > 500) {
|
|
|
+ canvas.drawText("Game Over", 200, 200);
|
|
|
+ }
|
|
|
+ this.update();
|
|
|
+ return
|
|
|
}
|
|
|
+
|
|
|
+ // 刷新游戏画面
|
|
|
this.update();
|
|
|
- return;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 绘制背景图
|
|
|
+ */
|
|
|
+ drawMap(painter: SPainter) {
|
|
|
+ // 遍历行
|
|
|
+ for (let row = 0; row < 4; row++) {
|
|
|
+ // 遍历列
|
|
|
+ for (let col = 0; col < 4; col++) {
|
|
|
+ this.drawCell(painter, row, col, this.gridMap[row][col]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 绘制数字
|
|
|
+ */
|
|
|
+ drawNum(painter: SPainter): void {
|
|
|
+ // 遍历行
|
|
|
+ for (let row = 0; row < 4; row++) {
|
|
|
+ // 遍历列
|
|
|
+ for (let col = 0; col < 4; col++) {
|
|
|
+ this.drawCellNum(painter, row, col, this.gridMap[row][col]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 绘制格子中的数字
|
|
|
+ */
|
|
|
+ drawCellNum(painter: SPainter, row: number, col: number, type: number): void {
|
|
|
+ const x = col * 100;
|
|
|
+ const y = row * 100;
|
|
|
+ if (type != 0) {
|
|
|
+ painter.font.textAlign = STextAlign.Center;
|
|
|
+ painter.font.size = 30;
|
|
|
+ // @ts-ignore
|
|
|
+ painter.brush.color = new SColor(this.color[type].font);
|
|
|
+ painter.drawText(type + '', x + 45, y + 45);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 绘制具体格子
|
|
|
+ *
|
|
|
+ * @param painter 绘制对象
|
|
|
+ * @param row 行
|
|
|
+ * @param col 列
|
|
|
+ * @param type 图类型
|
|
|
+ */
|
|
|
+ private drawCell(painter: SPainter, row: number, col: number, type: number): void {
|
|
|
+ const x = col * 100 + 10;
|
|
|
+ const y = row * 100 + 10;
|
|
|
+ painter.brush.color = new SColor(this.color[type].background);
|
|
|
+ painter.drawRoundRect(x, y, 90, 90, 10);
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|
|
|
@Component
|
|
|
export default class Gcanvas extends Vue {
|
|
|
- /** 实例化 */
|
|
|
- view: GameView | null = null;
|
|
|
+ /** 实例化 view */
|
|
|
+ view: GameView | null = null
|
|
|
|
|
|
/**
|
|
|
* 页面挂载完成
|
|
|
*/
|
|
|
mounted(): void {
|
|
|
- this.view = new GameView('canvas')
|
|
|
+ this.view = new GameView('canvas');
|
|
|
+ document.getElementById("canvas")!!.focus();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 重置操作
|
|
|
*/
|
|
|
reset(): void {
|
|
|
+ // 初始化游戏
|
|
|
this.view!!.init()
|
|
|
document.getElementById("canvas")!!.focus()
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
-<style scoped>
|
|
|
+<style scoped lang="less">
|
|
|
|
|
|
</style>
|