elsfkDifficult.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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. }
  217. /**
  218. * 键盘按下事件
  219. *
  220. * @param event 事件对象
  221. */
  222. onKeyDown(event: KeyboardEvent): void {
  223. if (this.gameOver) { // 游戏结束
  224. return;
  225. }
  226. if (event.code == "KeyW") { // 按键 w
  227. if (!this.checkKnocked(this.x, this.y, (this.dir + 1) % 4)) { // 检查变形后是否碰撞
  228. this.dir = (this.dir + 1) % 4;
  229. }
  230. } else if (event.code == "KeyA") { // 按键 a
  231. if (!this.checkKnocked(this.x - 1, this.y, this.dir)) { // 检查左移后是否碰撞
  232. this.x--;
  233. }
  234. } else if (event.code == "KeyD") { // 按键 d
  235. if (!this.checkKnocked(this.x + 1, this.y, this.dir)) { // 检查右移后是否碰撞
  236. this.x++;
  237. }
  238. } else if (event.code == "KeyS") { // 按键 s
  239. if (!this.checkKnocked(this.x, this.y + 1, this.dir)) { // 检查下移后是否碰撞
  240. this.y++;
  241. } else {
  242. this.fillMap();
  243. }
  244. }
  245. this.update();
  246. }
  247. /**
  248. * Item 绘制操作
  249. *
  250. * @param painter 绘制对象
  251. */
  252. onDraw(painter: SPainter): void {
  253. if (this.gameOver) { // 游戏结束绘制文字
  254. painter.brush.color = SColor.Blue;
  255. painter.font.size = 20;
  256. painter.font.textAlign = STextAlign.Center;
  257. painter.drawText("GAME OVER", 180, 30);
  258. return;
  259. }
  260. painter.clearRect(0, 0, this.width, this.height);
  261. painter.pen.color = new SColor("#0bc0ff");
  262. painter.brush.color = new SColor("#2accc7");
  263. this.drawMap(painter);
  264. painter.pen.color = this.boxColor;
  265. painter.brush.color = this.boxColor;
  266. // 绘制图形
  267. this.drawBox(painter);
  268. if (Date.now() - this.t > 500) { // 下落速度,下移一格
  269. if (!this.checkKnocked(this.x, this.y + 1, this.dir)) { // 下移是否碰撞
  270. this.y++;
  271. } else {
  272. this.fillMap();
  273. }
  274. this.t = Date.now();
  275. }
  276. this.update();
  277. }
  278. /**
  279. * 初始化背景
  280. */
  281. initMap(): void {
  282. this.gameOver = false;
  283. this.map = [];
  284. for (let row = 0; row < 22; row++) { // 循环行数
  285. const m1: number[] = [];
  286. for (let col = 0; col < 14; col++) { // 循环列数
  287. if (row > 19 || col < 2 || col > 11) { // 左侧,右侧,底部补充两个格
  288. // -1 代表左右填充
  289. m1.push(-1);
  290. } else {
  291. m1.push(0);
  292. }
  293. }
  294. this.map.push(m1);
  295. }
  296. }
  297. /**
  298. * 初始化方块形状
  299. */
  300. initBox(): void {
  301. this.x = 5;
  302. this.y = 0;
  303. this.boxType = Number(Math.floor(Math.random() * 7));
  304. this.dir = Number(Math.floor(Math.random() * 4));
  305. this.initColor();
  306. }
  307. /**
  308. * 初始化方块颜色
  309. */
  310. initColor(): void {
  311. const a = Number(Math.floor(Math.random() * 128));
  312. const b = Number(Math.floor(Math.random() * 128));
  313. const c = Number(Math.floor(Math.random() * 128));
  314. this.boxColor = new SColor(a, b, c);
  315. }
  316. /**
  317. * 绘制背景
  318. *
  319. * @param painter 绘制对象
  320. */
  321. private drawMap(painter: SPainter): void {
  322. for (let row = 0; row < 22; row++) { // 行数
  323. for (let col = 0; col < 14; col++) { // 列数
  324. this.drawShape(painter, row, col, this.map[row][col]);
  325. }
  326. }
  327. }
  328. /**
  329. * 绘制实体图形
  330. *
  331. * @param painter 绘制对象
  332. * @param row 行
  333. * @param col 列
  334. * @param type 图类型
  335. */
  336. private drawShape(painter: SPainter, row: number, col: number, type: number): void {
  337. const x = col * 30 + 11 - 60;
  338. const y = row * 30 + 11;
  339. switch (type) { // 类型判断
  340. case 0: // 空白
  341. break;
  342. case -1: // 边界
  343. painter.drawRoundRect(x, y, 28, 28, 6);
  344. break;
  345. default: // 填充
  346. painter.drawRoundRect(x, y, 28, 28, 6);
  347. break;
  348. }
  349. }
  350. /**
  351. * 绘制方块
  352. *
  353. * @param painter 绘制对象
  354. */
  355. private drawBox(painter: SPainter): void {
  356. for (let row = 0; row < 4; row++) { // 绘制图形行数
  357. for (let col = 0; col < 4; col++) { // 绘制图形列数
  358. this.drawShape(painter, row + this.y, col + this.x, this.box[this.boxType * 4 + this.dir][row][col]);
  359. }
  360. }
  361. }
  362. /**
  363. * 是否碰撞
  364. *
  365. * @param x x 坐标
  366. * @param y y 坐标
  367. * @param dir 方块变形索引
  368. * @return 是否碰撞
  369. */
  370. private checkKnocked(x: number, y: number, dir: number): boolean {
  371. for (let row = 0; row < 4; row++) { // 绘制图形行数
  372. for (let col = 0; col < 4; col++) { // 绘制图形列数
  373. // 判断是否重合,每种图形四个一组,四种变形的某种变形,不为零表示绘制(确定方块),坐标转换,背景图中绘制
  374. if (this.box[this.boxType * 4 + dir][row][col] != 0 && this.map[y + row][x + col] != 0) {
  375. return true;
  376. }
  377. }
  378. }
  379. return false;
  380. }
  381. /**
  382. * 将方块合到背景中
  383. */
  384. private fillMap(): void {
  385. for (let row = 0; row < 4; row++) { // 遍历图形的行数
  386. for (let col = 0; col < 4; col++) { // 遍历图形的列数
  387. if (this.box[this.boxType * 4 + this.dir][row][col] == 1) { // 判断图形绘制
  388. // 填充至背景中
  389. this.map[this.y + row][this.x + col] = 1;
  390. }
  391. }
  392. }
  393. this.initBox();
  394. if (this.checkKnocked(this.x, this.y, this.dir)) { // 判断背景上的图形与初始化图形是否重合
  395. // 游戏结束
  396. this.gameOver = true;
  397. }
  398. this.remove();
  399. }
  400. /**
  401. * 消除满行
  402. */
  403. private remove(): void {
  404. // 消除行数
  405. let removeCount = 0;
  406. // 循环行数,不包含底部填充2行
  407. for (let row = 0; row < 20; row++) {
  408. // 消除标记,true 是
  409. let flag = true;
  410. // 若当前行有一个空格,则不消除
  411. for (let col = 2; col < 12; col++) {
  412. if (this.map[row][col] == 0) { // 存在空格
  413. flag = false;
  414. break;
  415. }
  416. }
  417. if (flag) { // 标记删除
  418. this.map.splice(row, 1);
  419. // 顶部加一行空行
  420. this.map.unshift(this.randomRow(10, 0));
  421. removeCount++;
  422. }
  423. }
  424. if (removeCount > 0) { // 记录消除行数
  425. this.$emit("score", removeCount);
  426. // 删除行
  427. this.map.shift();
  428. // 底部随机产生一行
  429. this.map.splice(19, 0, this.randomRow());
  430. }
  431. }
  432. /**
  433. * 随机数组生成
  434. *
  435. * @return 数组
  436. */
  437. private randomRow(len: number = 10, num: number = 2): number[] {
  438. let array = new Array(len + 4).fill(-1);
  439. // 生成随机数,循环不包含左右填充格
  440. for (let i = 2; i < array.length - 2; i++) {
  441. array[i] = Math.floor(num * Math.random());
  442. }
  443. return array;
  444. }
  445. }
  446. @Component
  447. export default class ElsfkCanvas extends Vue {
  448. /** 图 id */
  449. id: string = uuid();
  450. /** 记分数组列表 */
  451. scoreList: number[] = [100, 300, 600, 1000];
  452. /** 记分分数 */
  453. score: number = 0;
  454. /** 实例化 view */
  455. view: TestView | undefined = undefined;
  456. /**
  457. * 页面挂载
  458. */
  459. mounted(): void {
  460. this.view = new TestView(this.id);
  461. this.view.connect("score", this, this.changeScore);
  462. document.getElementById(this.id)!!.focus();
  463. }
  464. /**
  465. * 分值展示
  466. *
  467. * @param v canvas 视图对象
  468. * @param count 计数分值
  469. */
  470. changeScore(v: SCanvasView, count: number[]) {
  471. //根据消除行数判断分值
  472. this.score += this.scoreList[count[0] - 1];
  473. }
  474. /**
  475. * 重置操作
  476. */
  477. reset() {
  478. this.view!!.initMap();
  479. this.view!!.initBox();
  480. this.score = 0;
  481. this.view!!.update();
  482. document.getElementById(this.id)!!.focus();
  483. }
  484. }
  485. </script>