elsfk.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <template>
  2. <div style="position: relative;">
  3. <canvas id="tetris1" width="320" height="620" style="border: 1px #ccc solid" tabindex="0"/>
  4. <div style="position: absolute;top: 0;left: 350px;">
  5. <p>{{view?view.score:0}}</p>
  6. <el-button @click="reset" type="primary">重新开始</el-button>
  7. </div>
  8. </div>
  9. </template>
  10. <script lang="ts">
  11. import { Component, Vue } from "vue-property-decorator";
  12. import { SCanvasView, SColor, SPainter, STextAlign } from "@persagy-web/draw/lib";
  13. /**
  14. * 俄罗斯方块视图
  15. *
  16. * @author 郝洁 <haojie@persagy.com>
  17. */
  18. class TetrisView extends SCanvasView {
  19. /** 网格数组 (二维数组初始化) */
  20. gridMap = Array(20)
  21. .fill(null)
  22. .map(() => Array(10).fill(0));
  23. /** 方块类型索引 0-6 */
  24. boxType = Number(Math.floor(Math.random() * 7));
  25. /** 方块变形索引 0-3 */
  26. dir = Number(Math.floor(Math.random() * 4));
  27. /** 下落方块行坐标 */
  28. currRow = 0;
  29. /** 下落方块列坐标 */
  30. currCol = 3;
  31. /** 记录上次刷新时间 */
  32. lastTime = Date.now();
  33. /** 记分分数 */
  34. score = 0;
  35. /** 是否游戏结束 */
  36. isGameOver = false;
  37. /** 所有方块集合 */
  38. box: number[][][]=[
  39. [
  40. [0, 0, 0, 0],
  41. [0, 1, 1, 0],
  42. [0, 1, 1, 0],
  43. [0, 0, 0, 0],
  44. ],
  45. [
  46. [0, 0, 0, 0],
  47. [0, 1, 1, 0],
  48. [0, 1, 1, 0],
  49. [0, 0, 0, 0],
  50. ],
  51. [
  52. [0, 0, 0, 0],
  53. [0, 1, 1, 0],
  54. [0, 1, 1, 0],
  55. [0, 0, 0, 0],
  56. ],
  57. [
  58. [0, 0, 0, 0],
  59. [0, 1, 1, 0],
  60. [0, 1, 1, 0],
  61. [0, 0, 0, 0],
  62. ],
  63. [
  64. [0, 1, 0, 0],
  65. [0, 1, 0, 0],
  66. [0, 1, 0, 0],
  67. [0, 1, 0, 0],
  68. ],
  69. [
  70. [0, 0, 0, 0],
  71. [1, 1, 1, 1],
  72. [0, 0, 0, 0],
  73. [0, 0, 0, 0],
  74. ],
  75. [
  76. [0, 0, 1, 0],
  77. [0, 0, 1, 0],
  78. [0, 0, 1, 0],
  79. [0, 0, 1, 0],
  80. ],
  81. [
  82. [0, 0, 0, 0],
  83. [0, 0, 0, 0],
  84. [1, 1, 1, 1],
  85. [0, 0, 0, 0],
  86. ],
  87. [
  88. [0, 0, 0, 0],
  89. [0, 1, 1, 0],
  90. [1, 1, 0, 0],
  91. [0, 0, 0, 0],
  92. ],
  93. [
  94. [0, 1, 0, 0],
  95. [0, 1, 1, 0],
  96. [0, 0, 1, 0],
  97. [0, 0, 0, 0],
  98. ],
  99. [
  100. [0, 0, 0, 0],
  101. [0, 0, 1, 1],
  102. [0, 1, 1, 0],
  103. [0, 0, 0, 0],
  104. ],
  105. [
  106. [0, 0, 0, 0],
  107. [0, 1, 0, 0],
  108. [0, 1, 1, 0],
  109. [0, 0, 1, 0],
  110. ],
  111. [
  112. [0, 0, 0, 0],
  113. [0, 1, 1, 0],
  114. [0, 0, 1, 1],
  115. [0, 0, 0, 0],
  116. ],
  117. [
  118. [0, 0, 0, 0],
  119. [0, 0, 1, 0],
  120. [0, 1, 1, 0],
  121. [0, 1, 0, 0],
  122. ],
  123. [
  124. [0, 0, 0, 0],
  125. [1, 1, 0, 0],
  126. [0, 1, 1, 0],
  127. [0, 0, 0, 0],
  128. ],
  129. [
  130. [0, 0, 1, 0],
  131. [0, 1, 1, 0],
  132. [0, 1, 0, 0],
  133. [0, 0, 0, 0],
  134. ],
  135. [
  136. [0, 0, 0, 0],
  137. [0, 1, 1, 0],
  138. [0, 0, 1, 0],
  139. [0, 0, 1, 0],
  140. ],
  141. [
  142. [0, 0, 0, 0],
  143. [0, 0, 1, 0],
  144. [1, 1, 1, 0],
  145. [0, 0, 0, 0],
  146. ],
  147. [
  148. [0, 1, 0, 0],
  149. [0, 1, 0, 0],
  150. [0, 1, 1, 0],
  151. [0, 0, 0, 0],
  152. ],
  153. [
  154. [0, 0, 0, 0],
  155. [0, 1, 1, 1],
  156. [0, 1, 0, 0],
  157. [0, 0, 0, 0],
  158. ],
  159. [
  160. [0, 0, 0, 0],
  161. [0, 1, 1, 0],
  162. [0, 1, 0, 0],
  163. [0, 1, 0, 0],
  164. ],
  165. [
  166. [0, 0, 0, 0],
  167. [1, 1, 1, 0],
  168. [0, 0, 1, 0],
  169. [0, 0, 0, 0],
  170. ],
  171. [
  172. [0, 0, 1, 0],
  173. [0, 0, 1, 0],
  174. [0, 1, 1, 0],
  175. [0, 0, 0, 0],
  176. ],
  177. [
  178. [0, 0, 0, 0],
  179. [0, 1, 0, 0],
  180. [0, 1, 1, 1],
  181. [0, 0, 0, 0],
  182. ],
  183. [
  184. [0, 0, 0, 0],
  185. [0, 0, 1, 0],
  186. [0, 1, 1, 1],
  187. [0, 0, 0, 0],
  188. ],
  189. [
  190. [0, 0, 0, 0],
  191. [0, 1, 0, 0],
  192. [0, 1, 1, 0],
  193. [0, 1, 0, 0],
  194. ],
  195. [
  196. [0, 0, 0, 0],
  197. [1, 1, 1, 0],
  198. [0, 1, 0, 0],
  199. [0, 0, 0, 0],
  200. ],
  201. [
  202. [0, 0, 1, 0],
  203. [0, 1, 1, 0],
  204. [0, 0, 1, 0],
  205. [0, 0, 0, 0],
  206. ]
  207. ];
  208. /** 记分数组列表 */
  209. private _scoreList = [100, 300, 600, 1000];
  210. /**
  211. * 构造函数
  212. *
  213. * @param id canvas DOM id
  214. */
  215. constructor(id: string) {
  216. super(id);
  217. // 初始化游戏
  218. this.init();
  219. } // Constructor
  220. /**
  221. * 初始化游戏
  222. */
  223. init(): void {
  224. // 遍历网格的行
  225. for (let r = 0; r < 20; r++) {
  226. // 遍历网格的列
  227. for (let c = 0; c < 10; c++) {
  228. // 将网格所有数据填充为 0
  229. this.gridMap[r][c] = 0;
  230. }
  231. }
  232. // 生成新的下落box
  233. this.newFallBox();
  234. // 初始化分数
  235. this.score = 0;
  236. // 游戏结束标记为 false
  237. this.isGameOver = false;
  238. }
  239. /**
  240. * 键盘按下事件
  241. *
  242. * @param event 事件对象
  243. */
  244. onKeyDown(event: KeyboardEvent): void {
  245. // 如果游戏结束, 不响应任何键
  246. if (this.isGameOver) {
  247. return;
  248. }
  249. if (event.code == "KeyW") { // 用户按下 W 键,变形
  250. // 如果变形不会碰撞
  251. if (!this.checkCollide(this.boxType, (this.dir + 1) % 4, this.currRow, this.currCol)) {
  252. // 进行变形操作
  253. this.dir = (this.dir + 1) % 4;
  254. }
  255. } else if (event.code == "KeyA") { // 用户按下 A 键,左移
  256. // 如果左移不会碰撞
  257. if (!this.checkCollide(this.boxType,this.dir, this.currRow, this.currCol - 1)) {
  258. // 左移
  259. this.currCol--;
  260. }
  261. } else if (event.code == "KeyD") { // 用户按下 D 键,右移
  262. // 如果右移不会碰撞
  263. if (!this.checkCollide(this.boxType,this.dir, this.currRow, this.currCol + 1)) {
  264. // 右移
  265. this.currCol++;
  266. }
  267. } else if (event.code == "KeyS") { // 用户按下 S 键,下移
  268. // box 下落
  269. this.fallBox();
  270. }
  271. // 更新视图
  272. this.update();
  273. }
  274. /**
  275. * 绘制游戏画面
  276. *
  277. * @param canvas 画布
  278. */
  279. onDraw(canvas: SPainter): void {
  280. // 清空画布
  281. canvas.clearRect(0, 0, this.width, this.height);
  282. // 绘制网格
  283. this.drawGridMap(canvas);
  284. // 如果游戏结束
  285. if (this.isGameOver) {
  286. // 闪烁显示 “Game Over”
  287. canvas.brush.color = SColor.Blue;
  288. canvas.font.size = 40;
  289. canvas.font.textAlign = STextAlign.Center;
  290. if (Date.now() % 1000 > 500) {
  291. canvas.drawText("Game Over", 160, 300);
  292. }
  293. // 刷新游戏画面
  294. this.update();
  295. return;
  296. }
  297. // 绘制下落的广场
  298. this.drawFallBox(canvas);
  299. // 判断是否到了 box 下落的时间(用时间间隔判断,不会受机器的性能影响)
  300. if (Date.now() - this.lastTime > 500) {
  301. // box 下落
  302. this.fallBox();
  303. this.lastTime = Date.now();
  304. }
  305. // 刷新游戏画面
  306. this.update();
  307. }
  308. /**
  309. * 绘制网格
  310. *
  311. * @param canvas 画布
  312. */
  313. private drawGridMap(canvas: SPainter): void {
  314. // 遍历行
  315. for (let r = 0; r < 20; r++) {
  316. // 遍历列
  317. for (let c = 0; c < 10; c++) {
  318. // 绘制小格
  319. this.drawGird(canvas, this.gridMap[r][c], r, c);
  320. }
  321. }
  322. }
  323. /**
  324. * 绘制下落的 Box
  325. *
  326. * @param canvas 画布
  327. */
  328. private drawFallBox(canvas: SPainter): void {
  329. // 遍历 box 的行
  330. for (let r = 0; r < 4; r++) {
  331. // 遍历 box 的列
  332. for (let c = 0; c < 4; c++) {
  333. // 绘制小格
  334. this.drawGird(canvas, this.box[this.boxType * 4 + this.dir][r][c], this.currRow + r, this.currCol + c);
  335. }
  336. }
  337. }
  338. /**
  339. * 执行 box 下落流程
  340. */
  341. private fallBox(): void {
  342. // 如果可以下移
  343. if (!this.checkCollide(this.boxType,this.dir, this.currRow + 1, this.currCol)) {
  344. // 下移并刷新游戏画面
  345. this.currRow++;
  346. this.update();
  347. return;
  348. }
  349. // 将 box 中的内容填充到网格
  350. this.boxIntoMap();
  351. // 如果 box 没有下移过(游戏结束条件)
  352. if (this.currRow <= -1) {
  353. // 游戏结束,并更新游戏画面
  354. this.isGameOver = true;
  355. this.update();
  356. return;
  357. }
  358. // 移除满行
  359. const rows = this.removeFullRow();
  360. // 如果移除大于 1 行
  361. if (rows > 0) {
  362. this.score += this._scoreList[rows - 1];
  363. }
  364. // 生成新的下落 box
  365. this.newFallBox();
  366. }
  367. /**
  368. * 画一个小格
  369. *
  370. * @param canvas 画布
  371. * @param type 小格的图类型
  372. * @param row 行
  373. * @param col 列
  374. */
  375. private drawGird(canvas: SPainter, type: number, row: number, col: number): void {
  376. // 如果类型不为 0
  377. if (type != 0) {
  378. // 绘制小格
  379. canvas.pen.color = SColor.Transparent;
  380. canvas.brush.color = SColor.Red;
  381. canvas.drawRect(col * 30 + 11, row * 30 + 11, 28, 28);
  382. }
  383. }
  384. /**
  385. * 判断当前下落的方块是否会发生碰撞
  386. *
  387. * @param type 下落广场的类型
  388. * @param dir 方块的朝向
  389. * @param row 下落方块的行坐标
  390. * @param col 下落广场的列坐标
  391. * @return 碰撞返回 true,否则返回 false 。
  392. */
  393. private checkCollide(type: number, dir: number, row: number, col: number): boolean {
  394. // 遍历 box 的行
  395. for (let r = 0; r < 4; r++) {
  396. const r1 = row + r;
  397. // 遍历 box 的列
  398. for (let c = 0; c < 4; c++) {
  399. const c1 = col + c;
  400. if (this.box[this.boxType * 4 + dir][r][c] != 0) {
  401. // 如果没有对应的网络格(下标越界),则为碰撞
  402. if (c1 < 0 || c1 >= 10 || r1 >= 20) {
  403. return true;
  404. }
  405. // 如果对应的网格不为 0,则为碰撞
  406. if (r1 > 0 && this.gridMap[row + r][col + c] != 0) {
  407. return true;
  408. }
  409. }
  410. }
  411. }
  412. // 返回不发生碰撞
  413. return false;
  414. }
  415. /**
  416. * 当前下落 box 填充到网格
  417. */
  418. private boxIntoMap() {
  419. // 遍历 box 的行
  420. for (let r = 0; r < 4; r++) {
  421. const r1 = this.currRow + r;
  422. // 遍历 box 的列
  423. for (let c = 0; c < 4; c++) {
  424. const g = this.box[this.boxType * 4 + this.dir][r][c]
  425. const c1 = this.currCol + c;
  426. // 如果下落的 box 对应的格不为空
  427. if (g != 0) {
  428. // 如果没有对应的网格(下标越界),则处理下一格
  429. if (c1 < 0 || c1 >= 10 || r1 < 0 || r1 >= 20) {
  430. continue;
  431. }
  432. // 将 box 填入到网格的对应位置
  433. this.gridMap[r1][c1] = g;
  434. }
  435. }
  436. }
  437. }
  438. /**
  439. * 生成新的下落方块
  440. */
  441. private newFallBox() {
  442. // box 的初始位置
  443. this.currCol = 3;
  444. this.currRow = -3;
  445. // box 的类型与方向
  446. this.boxType = Number(Math.floor(Math.random() * 7));
  447. this.dir = Number(Math.floor(Math.random() * 4));
  448. }
  449. /**
  450. * 消除满行(即不存在空格的行)
  451. *
  452. * @return 消除的行数
  453. */
  454. private removeFullRow(): number {
  455. // 用于统计消除的行数
  456. let count = 0;
  457. // 遍历行
  458. for (let r = 0; r < 20; r++) {
  459. // 消除标记
  460. let isRemove = true;
  461. // 遍历列
  462. for (let c = 0; c < 10; c++) {
  463. // 有任意一格为空,则标识消除标记为 false,跳出循环
  464. if (this.gridMap[r][c] == 0) {
  465. isRemove = false
  466. break;
  467. }
  468. }
  469. // 如果消除标记为 true
  470. if (isRemove) {
  471. // 清除 row 行。
  472. this.gridMap.splice(r,1);
  473. this.gridMap.unshift(Array(10).fill(0));
  474. // 统计消除的行数
  475. count++;
  476. }
  477. }
  478. // 返回消除的行数
  479. return count
  480. }
  481. }
  482. /**
  483. * 俄罗斯方块 demo
  484. *
  485. * @author 郝洁 <haojie@persagy.com>
  486. */
  487. @Component
  488. export default class Tetris1 extends Vue {
  489. /** 实例化 view */
  490. view: TetrisView | null = null;
  491. /**
  492. * 页面挂载完成
  493. */
  494. mounted(): void {
  495. this.view = new TetrisView("tetris1");
  496. document.getElementById("tetris1")!!.focus();
  497. }
  498. /**
  499. * 重置操作
  500. */
  501. reset(): void {
  502. // 初始化游戏
  503. this.view!!.init();
  504. document.getElementById("tetris1")!!.focus();
  505. }
  506. }
  507. </script>