2048.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. <template>
  2. <div style="position: relative;">
  3. <canvas id="canvas" width="400" height="400" style="border: 1px #ccc solid;background: #bbada0" tabindex="0"/>
  4. <div style="position: absolute;top: 0;left: 450px;">
  5. <p>分数:{{ view ? view.score : 0 }}</p>
  6. <el-button @click="reset" type="primary">重新开始</el-button>
  7. <p>W:上移</p>
  8. <p>S:下移</p>
  9. <p>A:左移</p>
  10. <p>D:右移</p>
  11. </div>
  12. </div>
  13. </template>
  14. <script lang="ts">
  15. import {Component, Vue} from "vue-property-decorator"
  16. import {SCanvasView, SColor, SPainter, STextAlign, STextBaseLine} from "@persagy-web/draw/lib";
  17. /**
  18. * 游戏 2048
  19. *
  20. * @author 郝洁 <haojie@persagy.com>
  21. */
  22. class GameView extends SCanvasView {
  23. /** 网格数组初始化 */
  24. gridMap = Array(4).fill(null).map(() => Array(4).fill(0))
  25. /** 游戏结束标识 */
  26. gameOver: boolean = false
  27. /** 游戏累计分数 */
  28. score = 0;
  29. /** 数字画笔 */
  30. // numPaint = new SPainter();
  31. /** 文字画笔 */
  32. // textPaint = new SPainter();
  33. /** 格子顏色 */
  34. color = {
  35. 0: {
  36. font: "#ffffffff",
  37. background: "#eee4da59"
  38. },
  39. 2: {
  40. font: "#776e65",
  41. background: "#eee4da"
  42. },
  43. 4: {
  44. font: "#776e65",
  45. background: "#ede0c8"
  46. },
  47. 8: {
  48. font: "#f9f6f2",
  49. background: "#f2b179"
  50. },
  51. 16: {
  52. font: "#f9f6f2",
  53. background: "#f59563"
  54. },
  55. 32: {
  56. font: "#f9f6f2",
  57. background: "#f67c5f"
  58. },
  59. 64: {
  60. font: "#f9f6f2",
  61. background: "#f65e3b"
  62. },
  63. 128: {
  64. font: "#f9f6f2",
  65. background: "#edcf72"
  66. },
  67. 256: {
  68. font: "#f9f6f2",
  69. background: "#edcc61"
  70. },
  71. 512: {
  72. font: "#f9f6f2",
  73. background: "#edc850"
  74. },
  75. 1024: {
  76. font: "#f9f6f2",
  77. background: "#edc53f"
  78. },
  79. 2048: {
  80. font: "#f9f6f2",
  81. background: "#edc22e"
  82. },
  83. };
  84. /**
  85. * 构造函数
  86. *
  87. * @param id canvas DOM id
  88. */
  89. constructor(id: string) {
  90. super(id);
  91. // 初始化游戏
  92. this.init()
  93. }
  94. /**
  95. * 初始化游戏
  96. */
  97. init(): void {
  98. // 遍历网格的行
  99. for (let r = 0; r < 4; r++) {
  100. // 遍历网格的列
  101. for (let c = 0; c < 4; c++) {
  102. this.gridMap[r][c] = 0
  103. }
  104. }
  105. // 随机填充两个 2
  106. this.gridMap[Math.floor(Math.random() * 4)][Math.floor(Math.random() * 4)] = 2
  107. this.gridMap[Math.floor(Math.random() * 4)][Math.floor(Math.random() * 4)] = 2
  108. // 初始化游戏分数
  109. this.score = 0
  110. // 游戏结束标识标记
  111. this.gameOver = false
  112. }
  113. /**
  114. * 键盘按下事件
  115. *
  116. * @param event 事件对象
  117. */
  118. onKeyDown(event: KeyboardEvent): void {
  119. if (this.gameOver) {
  120. return;
  121. }
  122. // 按键判断
  123. switch (event.code) {
  124. case "KeyW": // 上移动作
  125. case "ArrowUp":
  126. // 判断游戏是否继续,底部产生随机位置随机数
  127. this.moveUp();
  128. break;
  129. case "KeyA": // 左移动作
  130. case "ArrowLeft":
  131. // 判断游戏是否继续,右部产生随机位置随机数
  132. this.moveLeft();
  133. break;
  134. case "KeyD": // 右移动作
  135. case "ArrowRight":
  136. this.moveRight();
  137. // 判断游戏是否继续,左部产生随机位置随机数
  138. break;
  139. // 按键 下
  140. case "KeyS": // 下移动作
  141. case "ArrowDown":
  142. // 判断游戏是否继续,上部产生随机位置随机数
  143. this.moveDown();
  144. break;
  145. default :
  146. // do nothing
  147. }
  148. // 更新视图
  149. this.update();
  150. }
  151. /**
  152. * 上移
  153. */
  154. moveUp(): void {
  155. // 数字是否移动
  156. let moved = false
  157. // 遍历所有的列
  158. for (let i = 0; i < 4; i++) {
  159. // 进行合并操作的列表
  160. const numArr = Array(4)
  161. // 依次从网格中取要合并的元素
  162. for (let j = 0; j < 4; j++) {
  163. // 取数
  164. numArr[j] = this.gridMap[j][i]
  165. }
  166. // 移动与合并
  167. if (this.moveNum(numArr)) {
  168. moved = true;
  169. // 合并后的下列表反填入网格
  170. for (let j = 0; j < 4; j++) {
  171. // 反填
  172. this.gridMap[j][i] = numArr[j]
  173. }
  174. }
  175. }
  176. // 如果移动了数字
  177. if (moved) {
  178. // 索引列表
  179. const indexArr = []
  180. // 依次从网格取空格元素的下标索引
  181. for (let i = 0; i < 4; i++) {
  182. // 如果为空格
  183. if (this.gridMap[3][i] == 0) {
  184. // 保存下标
  185. indexArr.push(i)
  186. }
  187. }
  188. // 生成新的数的下标
  189. const index = this.newIndex(indexArr);
  190. if (index >= 0) {
  191. this.gridMap[3][index] = this.newNum()
  192. }
  193. }
  194. // 测试是否游戏结束
  195. this.gameOver = this.checkGameover()
  196. }
  197. /**
  198. * 下移
  199. */
  200. moveDown(): void {
  201. // 是否移动了数字
  202. let moved = false;
  203. // 遍历所有列
  204. for (let i = 0; i < 4; i++) {
  205. // 进行合并操作的列表
  206. const numArr = Array(4);
  207. // 依次从网格中取要合并的元素
  208. for (let j = 0; j < 4; j++) {
  209. // 取数
  210. numArr[j] = this.gridMap[3 - j][i];
  211. }
  212. // 移动与合并
  213. if (this.moveNum(numArr)) {
  214. moved = true;
  215. // 合并后的下列表反填入网格
  216. for (let j = 0; j < 4; j++) {
  217. // 反填
  218. this.gridMap[3 - j][i] = numArr[j];
  219. }
  220. }
  221. }
  222. // 如果移动了数字
  223. if (moved) {
  224. // 索引列表
  225. const indexArr = [];
  226. // 依次从网格取空格元素的下标索引
  227. for (let i = 0; i < 4; i++) {
  228. // 如果为空格
  229. if (this.gridMap[0][i] == 0) {
  230. // 保存下标
  231. indexArr.push(i)
  232. }
  233. }
  234. // 新生成的数的下标
  235. const index = this.newIndex(indexArr);
  236. if (index >= 0) {
  237. this.gridMap[0][index] = this.newNum();
  238. }
  239. }
  240. // 测试是否游戏结束
  241. this.gameOver = this.checkGameover();
  242. }
  243. /**
  244. * 左移
  245. */
  246. moveLeft(): void {
  247. // 是否移动了数字
  248. let moved = false;
  249. // 遍历所有行
  250. for (let i = 0; i < 4; i++) {
  251. // 进行合并操作的列表
  252. const numArr = Array(4);
  253. // 依次从网格中取要合并的元素
  254. for (let j = 0; j < 4; j++) {
  255. // 取数
  256. numArr[j] = this.gridMap[i][j];
  257. }
  258. // 移动与合并
  259. if (this.moveNum(numArr)) {
  260. moved = true;
  261. // 合并后的下列表反填入网格
  262. for (let j = 0; j < 4; j++) {
  263. // 反填
  264. this.gridMap[i][j] = numArr[j];
  265. }
  266. }
  267. }
  268. // 如果移动了数字
  269. if (moved) {
  270. // 索引列表
  271. const indexArr = [];
  272. // 依次从网格取空格元素的下标索引
  273. for (let i = 0; i < 4; i++) {
  274. // 如果为空格
  275. if (this.gridMap[i][3] == 0) {
  276. // 保存下标
  277. indexArr.push(i)
  278. }
  279. }
  280. // 新生成的数的下标
  281. const index = this.newIndex(indexArr);
  282. if (index >= 0) {
  283. this.gridMap[index][3] = this.newNum();
  284. }
  285. }
  286. // 测试是否游戏结束
  287. this.gameOver = this.checkGameover();
  288. }
  289. /**
  290. * 右移移
  291. */
  292. moveRight(): void {
  293. // 是否移动了数字
  294. let moved = false;
  295. // 遍历所有行
  296. for (let i = 0; i < 4; i++) {
  297. // 进行合并操作的列表
  298. const numArr = Array(4);
  299. // 依次从网格中取要合并的元素
  300. for (let j = 0; j < 4; j++) {
  301. // 取数
  302. numArr[j] = this.gridMap[i][3 - j];
  303. }
  304. // 移动与合并
  305. if (this.moveNum(numArr)) {
  306. moved = true;
  307. // 合并后的下列表反填入网格
  308. for (let j = 0; j < 4; j++) {
  309. // 反填
  310. this.gridMap[i][3 - j] = numArr[j];
  311. }
  312. }
  313. }
  314. // 如果移动了数字
  315. if (moved) {
  316. // 索引列表
  317. const indexArr = [];
  318. // 依次从网格取空格元素的下标索引
  319. for (let i = 0; i < 4; i++) {
  320. // 如果为空格
  321. if (this.gridMap[i][0] == 0) {
  322. // 保存下标
  323. indexArr.push(i)
  324. }
  325. }
  326. // 新生成的数的下标
  327. const index = this.newIndex(indexArr);
  328. if (index >= 0) {
  329. this.gridMap[index][0] = this.newNum();
  330. }
  331. }
  332. // 测试是否游戏结束
  333. this.gameOver = this.checkGameover();
  334. }
  335. /**
  336. * 移动或合并数字,数据相同则合并
  337. *
  338. * @param numArr 长度为 4 的数组
  339. * @return 移动数字返回 true,否则返回 false
  340. */
  341. private moveNum(numArr: number[]): boolean {
  342. // 记录所有合并数字的和
  343. let moved = false;
  344. // 从第二格开始取数
  345. for (let i = 1; i < numArr.length; i++) {
  346. const num = numArr[i];
  347. // 如果选定的格为0,则取下一格
  348. if (num == 0) {
  349. continue;
  350. }
  351. // 将除去的数往前移
  352. for (let j = i = 1; j >= 0; j--) {
  353. if (numArr[j] == 0) {
  354. // 前移
  355. numArr[j] = num;
  356. numArr[j + 1] = 0;
  357. moved = true
  358. } else if (numArr[j] == num) {
  359. // 合并
  360. numArr[j] += num;
  361. numArr[j + 1] = 0;
  362. this.score += num * 2;
  363. moved = true;
  364. break;
  365. } else {
  366. break;
  367. }
  368. }
  369. }
  370. // 返回所有合并的数组的和
  371. return moved
  372. }
  373. /**
  374. * 新生成数字的下标
  375. *
  376. * @param indexArr 空格的下标列表的长度大于 0,返回随机的下标;否则返回 0。
  377. */
  378. private newIndex(indexArr: number[]): number {
  379. return indexArr.length > 0 ? indexArr[Math.floor(Math.random() * indexArr.length)] : -1
  380. }
  381. /**
  382. * 返回新生成的数(2 或 4)
  383. *
  384. * @return 新生成的数(2 或 4)
  385. */
  386. private newNum(): number {
  387. // 30% 的概率为 4,70% 概率为 2。调整比例可增加或减小游戏的难度。
  388. return Math.random() > 0.3 ? 4 : 2
  389. }
  390. /**
  391. * 检查是否游戏结束。(没有为空的格,没有相邻两格的数字相同)
  392. *
  393. * @return 如果结束返回 true,否则返回 false;
  394. */
  395. private checkGameover(): boolean {
  396. // 依次遍历行与列
  397. for (let i = 0; i < 4; i++) {
  398. // 依次取相邻(横向/纵向)的两个格的数字
  399. for (let j = 0; j < 3; j++) {
  400. // 判断相邻的格是否相同,或是否为空。
  401. if (this.gridMap[i][j] == this.gridMap[i][j + 1] || this.gridMap[i][j] == 0 || this.gridMap[i][j + 1] == 0
  402. || this.gridMap[j][i] == this.gridMap[j + 1][i] || this.gridMap[j][i] == 0 || this.gridMap[j + 1][i] == 0) {
  403. // 返回游戏未结束
  404. return false
  405. }
  406. }
  407. }
  408. // 返回游戏结束
  409. return true
  410. }
  411. /**
  412. * 绘制游戏画面
  413. *
  414. * @param canvas 绘制对象
  415. */
  416. onDraw(canvas: SPainter): void {
  417. canvas.clearRect(0, 0, this.width, this.height)
  418. canvas.pen.color = SColor.Transparent;
  419. this.drawMap(canvas);
  420. this.drawNum(canvas);
  421. //如果游戏结束,绘制格子
  422. if (this.gameOver) {
  423. canvas.brush.color = SColor.Red;
  424. canvas.font.size = 40;
  425. canvas.font.textAlign = STextAlign.Center;
  426. if (Date.now() % 100 > 500) {
  427. canvas.drawText("Game Over", 200, 200);
  428. }
  429. this.update();
  430. return
  431. }
  432. // 刷新游戏画面
  433. this.update();
  434. }
  435. /**
  436. * 绘制背景图
  437. */
  438. drawMap(painter: SPainter) {
  439. // 遍历行
  440. for (let row = 0; row < 4; row++) {
  441. // 遍历列
  442. for (let col = 0; col < 4; col++) {
  443. this.drawCell(painter, row, col, this.gridMap[row][col]);
  444. }
  445. }
  446. }
  447. /**
  448. * 绘制数字
  449. */
  450. drawNum(painter: SPainter): void {
  451. // 遍历行
  452. for (let row = 0; row < 4; row++) {
  453. // 遍历列
  454. for (let col = 0; col < 4; col++) {
  455. this.drawCellNum(painter, row, col, this.gridMap[row][col]);
  456. }
  457. }
  458. }
  459. /**
  460. * 绘制格子中的数字
  461. */
  462. drawCellNum(painter: SPainter, row: number, col: number, type: number): void {
  463. const x = col * 100;
  464. const y = row * 100;
  465. if (type != 0) {
  466. painter.font.textAlign = STextAlign.Center;
  467. painter.font.size = 30;
  468. // @ts-ignore
  469. painter.brush.color = new SColor(this.color[type].font);
  470. painter.drawText(type + '', x + 45, y + 45);
  471. }
  472. }
  473. /**
  474. * 绘制具体格子
  475. *
  476. * @param painter 绘制对象
  477. * @param row 行
  478. * @param col 列
  479. * @param type 图类型
  480. */
  481. private drawCell(painter: SPainter, row: number, col: number, type: number): void {
  482. const x = col * 100 + 10;
  483. const y = row * 100 + 10;
  484. painter.brush.color = new SColor(this.color[type].background);
  485. painter.drawRoundRect(x, y, 90, 90, 10);
  486. }
  487. }
  488. @Component
  489. export default class Gcanvas extends Vue {
  490. /** 实例化 view */
  491. view: GameView | null = null
  492. /**
  493. * 页面挂载完成
  494. */
  495. mounted(): void {
  496. this.view = new GameView('canvas');
  497. document.getElementById("canvas")!!.focus();
  498. }
  499. /**
  500. * 重置操作
  501. */
  502. reset(): void {
  503. // 初始化游戏
  504. this.view!!.init()
  505. document.getElementById("canvas")!!.focus()
  506. }
  507. }
  508. </script>
  509. <style scoped lang="less">
  510. </style>