elsfk.vue 14 KB

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