123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- <template>
- <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, STextBaseLine} from "@persagy-web/draw/lib";
- /**
- * 游戏 2048
- *
- * @author 郝洁 <haojie@persagy.com>
- */
- class GameView extends SCanvasView {
- /** 网格数组初始化 */
- gridMap = Array(4).fill(null).map(() => Array(4).fill(0))
- /** 游戏结束标识 */
- gameOver: boolean = false
- /** 游戏累计分数 */
- 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"
- },
- };
- /**
- * 构造函数
- *
- * @param id canvas DOM id
- */
- constructor(id: string) {
- super(id);
- // 初始化游戏
- this.init()
- }
- /**
- * 初始化游戏
- */
- 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 事件对象
- */
- onKeyDown(event: KeyboardEvent): void {
- if (this.gameOver) {
- return;
- }
- // 按键判断
- switch (event.code) {
- 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();
- }
- /**
- * 上移
- */
- 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()
- }
- /**
- * 下移
- */
- moveDown(): 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[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 numArr 长度为 4 的数组
- * @return 移动数字返回 true,否则返回 false
- */
- 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 绘制对象
- */
- 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;
- if (Date.now() % 100 > 500) {
- canvas.drawText("Game Over", 200, 200);
- }
- this.update();
- return
- }
- // 刷新游戏画面
- this.update();
- }
- /**
- * 绘制背景图
- */
- 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 */
- view: GameView | null = null
- /**
- * 页面挂载完成
- */
- mounted(): void {
- this.view = new GameView('canvas');
- document.getElementById("canvas")!!.focus();
- }
- /**
- * 重置操作
- */
- reset(): void {
- // 初始化游戏
- this.view!!.init()
- document.getElementById("canvas")!!.focus()
- }
- }
- </script>
- <style scoped lang="less">
- </style>
|