elsfkDifficult.vue 14 KB

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