123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554 |
- <template>
- <div style="position: relative;">
- <canvas id="tetris1" width="320" height="620" style="border: 1px #ccc solid" tabindex="0"/>
- <div style="position: absolute;top: 0;left: 350px;">
- <p>{{view?view.score:0}}</p>
- <button @click="reset">重新开始</button>
- </div>
- </div>
- </template>
- <script lang="ts">
- import { Component, Vue } from "vue-property-decorator";
- import { SColor } from "sybotan-base";
- import { SCanvas, SCanvasView, SPaint } from "sybotan-graph";
- import { SPaintStyle } from "sybotan-graph/lib";
- /**
- * 俄罗斯广场视图
- *
- * @author 庞利祥 <sybotan@126.com>
- */
- class TetrisView extends SCanvasView {
- /** 小格画笔 */
- gridPaint = new SPaint();
- /** 文字画笔 */
- textPaint = new SPaint();
- /** 网格数组 (二维数组初始化) */
- gridMap = Array(20)
- .fill(null)
- .map(() => Array(10).fill(0));
- /** 方块类型索引 0-6 */
- boxType = Number(Math.floor(Math.random() * 7));
- /** 方块变形索引 0-3 */
- dir = Number(Math.floor(Math.random() * 4));
- /** 下落方块行坐标 */
- currRow = 0;
- /** 下落方块列坐标 */
- currCol = 3;
- /** 记录上次刷新时间 */
- lastTime = Date.now();
- /** 记分分数 */
- score = 0;
- /** 是否游戏结束 */
- isGameOver = false;
- /** 所有方块集合 */
- box: number[][][]=[
- [
- [0, 0, 0, 0],
- [0, 1, 1, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 1, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 1, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 1, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 1, 0, 0],
- [0, 1, 0, 0],
- [0, 1, 0, 0],
- [0, 1, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [1, 1, 1, 1],
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 1, 0],
- [0, 0, 1, 0],
- [0, 0, 1, 0],
- [0, 0, 1, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 0, 0, 0],
- [1, 1, 1, 1],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 1, 0],
- [1, 1, 0, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 1, 0, 0],
- [0, 1, 1, 0],
- [0, 0, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 0, 1, 1],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 0, 0],
- [0, 1, 1, 0],
- [0, 0, 1, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 1, 0],
- [0, 0, 1, 1],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 0, 1, 0],
- [0, 1, 1, 0],
- [0, 1, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [1, 1, 0, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 1, 0],
- [0, 1, 1, 0],
- [0, 1, 0, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 1, 0],
- [0, 0, 1, 0],
- [0, 0, 1, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 0, 1, 0],
- [1, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 1, 0, 0],
- [0, 1, 0, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 1, 1],
- [0, 1, 0, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 1, 0],
- [0, 1, 0, 0],
- [0, 1, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [1, 1, 1, 0],
- [0, 0, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 1, 0],
- [0, 0, 1, 0],
- [0, 1, 1, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 0, 0],
- [0, 1, 1, 1],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 0, 1, 0],
- [0, 1, 1, 1],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [0, 1, 0, 0],
- [0, 1, 1, 0],
- [0, 1, 0, 0],
- ],
- [
- [0, 0, 0, 0],
- [1, 1, 1, 0],
- [0, 1, 0, 0],
- [0, 0, 0, 0],
- ],
- [
- [0, 0, 1, 0],
- [0, 1, 1, 0],
- [0, 0, 1, 0],
- [0, 0, 0, 0],
- ]
- ];
- // /** 记分数组列表 */
- private _scoreList = [100, 300, 600, 1000];
- /**
- * 构造函数
- *
- * @param id canvas DOM id
- */
- constructor(id: string) {
- super(id);
- // 设置绘制网格的颜色
- this.gridPaint.fill = SColor.Red;
- this.gridPaint.style = SPaintStyle.Fill;
- this.textPaint.fill = SColor.Blue;
- this.textPaint.font.size = 40;
- // 初始化游戏
- this.init();
- }
- /**
- * 初始化游戏
- */
- init(): void {
- // 遍历网格的行
- for (let r = 0; r < 20; r++) {
- // 遍历网格的列
- for (let c = 0; c < 10; c++) {
- // 将网格所有数据填充为 0
- this.gridMap[r][c] = 0;
- }
- }
- // 生成新的下落box
- this.newFallBox();
- // 初始化分数
- this.score = 0;
- // 游戏结束标记为 false
- this.isGameOver = false;
- }
- /**
- * 键盘按下事件
- *
- * @param event 事件对象
- */
- onKeyDown(event: KeyboardEvent): void {
- // 如果游戏结束, 不响应任何键
- if (this.isGameOver) {
- return;
- }
- if (event.code == "KeyW") { // 用户按下 W 键,变形
- // 如果变形不会碰撞
- if (!this.checkCollide(this.boxType, (this.dir + 1) % 4, this.currRow, this.currCol)) {
- // 进行变形操作
- this.dir = (this.dir + 1) % 4;
- }
- } else if (event.code == "KeyA") { // 用户按下 A 键,左移
- // 如果左移不会碰撞
- if (!this.checkCollide(this.boxType,this.dir, this.currRow, this.currCol - 1)) {
- // 左移
- this.currCol--;
- }
- } else if (event.code == "KeyD") { // 用户按下 D 键,右移
- // 如果右移不会碰撞
- if (!this.checkCollide(this.boxType,this.dir, this.currRow, this.currCol + 1)) {
- // 右移
- this.currCol++;
- }
- } else if (event.code == "KeyS") { // 用户按下 S 键,下移
- // box 下落
- this.fallBox();
- }
- // 更新视图
- this.update();
- }
- /**
- * 绘制游戏画面
- *
- * @param canvas 画布
- */
- onDraw(canvas: SCanvas): void {
- // 清空画布
- this.clear();
- // 绘制网格
- this.drawGridMap(canvas);
- // 如果游戏结束
- if (this.isGameOver) {
- // 闪烁显示 “Game Over”
- if (Date.now() % 1000 > 500) {
- canvas.drawText("Game Over", 50, 300, this.textPaint);
- }
- // 刷新游戏画面
- this.update();
- return;
- }
- // 绘制下落的广场
- this.drawFallBox(canvas);
- // 判断是否到了 box 下落的时间(用时间间隔判断,不会受机器的性能影响)
- if (Date.now() - this.lastTime > 500) {
- // box 下落
- this.fallBox();
- this.lastTime = Date.now();
- }
- // 刷新游戏画面
- this.update();
- }
- /**
- * 绘制网格
- *
- * @param canvas 画布
- */
- private drawGridMap(canvas: SCanvas): void {
- // 遍历行
- for (let r = 0; r < 20; r++) {
- // 遍历列
- for (let c = 0; c < 10; c++) {
- // 绘制小格
- this.drawGird(canvas, this.gridMap[r][c], r, c);
- }
- }
- }
- /**
- * 绘制下落的 Box
- *
- * @param canvas 画布
- */
- private drawFallBox(canvas: SCanvas): void {
- // 遍历 box 的行
- for (let r = 0; r < 4; r++) {
- // 遍历 box 的列
- for (let c = 0; c < 4; c++) {
- // 绘制小格
- this.drawGird(canvas, this.box[this.boxType * 4 + this.dir][r][c], this.currRow + r, this.currCol + c);
- }
- }
- }
- /**
- * 执行 box 下落流程
- */
- private fallBox(): void {
- // 如果可以下移
- if (!this.checkCollide(this.boxType,this.dir, this.currRow + 1, this.currCol)) {
- // 下移并刷新游戏画面
- this.currRow++;
- this.update();
- return;
- }
- // 将 box 中的内容填充到网格
- this.boxIntoMap();
- // 如果 box 没有下移过(游戏结束条件)
- if (this.currRow <= -1) {
- // 游戏结束,并更新游戏画面
- this.isGameOver = true;
- this.update();
- return;
- }
- // 移除满行
- const rows = this.removeFullRow();
- // 如果移除大于 1 行
- if (rows > 0) {
- this.score += this._scoreList[rows - 1];
- }
- // 生成新的下落 box
- this.newFallBox();
- }
- /**
- * 画一个小格
- *
- * @param canvas 画布
- * @param type 小格的图类型
- * @param row 行
- * @param col 列
- */
- private drawGird(canvas: SCanvas, type: number, row: number, col: number): void {
- // 如果类型不为 0
- if (type != 0) {
- // 绘制小格
- canvas.drawRect(col * 30 + 11, row * 30 + 11, 28, 28, this.gridPaint);
- }
- }
- /**
- * 判断当前下落的方块是否会发生碰撞
- *
- * @param type 下落广场的类型
- * @param dir 方块的朝向
- * @param row 下落方块的行坐标
- * @param col 下落广场的列坐标
- * @return 碰撞返回 true,否则返回 false 。
- */
- private checkCollide(type: number, dir: number, row: number, col: number): boolean {
- // 遍历 box 的行
- for (let r = 0; r < 4; r++) {
- const r1 = row + r;
- // 遍历 box 的列
- for (let c = 0; c < 4; c++) {
- const c1 = col + c;
- if (this.box[this.boxType * 4 + dir][r][c] != 0) {
- // 如果没有对应的网络格(下标越界),则为碰撞
- if (c1 < 0 || c1 >= 10 || r1 >= 20) {
- return true;
- }
- // 如果对应的网格不为 0,则为碰撞
- if (r1 > 0 && this.gridMap[row + r][col + c] != 0) {
- return true;
- }
- }
- }
- }
- // 返回不发生碰撞
- return false;
- }
- /**
- * 当前下落 box 填充到网格
- */
- private boxIntoMap() {
- // 遍历 box 的行
- for (let r = 0; r < 4; r++) {
- const r1 = this.currRow + r;
- // 遍历 box 的列
- for (let c = 0; c < 4; c++) {
- const g = this.box[this.boxType * 4 + this.dir][r][c]
- const c1 = this.currCol + c;
- // 如果下落的 box 对应的格不为空
- if (g != 0) {
- // 如果没有对应的网格(下标越界),则处理下一格
- if (c1 < 0 || c1 >= 10 || r1 < 0 || r1 >= 20) {
- continue;
- }
- // 将 box 填入到网格的对应位置
- this.gridMap[r1][c1] = g;
- }
- }
- }
- }
- /**
- * 生成新的下落方块
- */
- private newFallBox() {
- // box 的初始位置
- this.currCol = 3;
- this.currRow = -3;
- // box 的类型与方向
- this.boxType = Number(Math.floor(Math.random() * 7));
- this.dir = Number(Math.floor(Math.random() * 4));
- }
- /**
- * 消除满行(即不存在空格的行)
- *
- * @return 消除的行数
- */
- private removeFullRow(): number {
- // 用于统计消除的行数
- let count = 0;
- // 遍历行
- for (let r = 0; r < 20; r++) {
- // 消除标记
- let isRemove = true;
- // 遍历列
- for (let c = 0; c < 10; c++) {
- // 有任意一格为空,则标识消除标记为 false,跳出循环
- if (this.gridMap[r][c] == 0) {
- isRemove = false
- break;
- }
- }
- // 如果消除标记为 true
- if (isRemove) {
- // 清除 row 行。
- this.gridMap.splice(r,1);
- this.gridMap.unshift(Array(10).fill(0));
- // 统计消除的行数
- count++;
- }
- }
- // 返回消除的行数
- return count
- }
- }
- /**
- * 俄罗斯方块 demo
- *
- * @author 庞利祥 <sybotan@126.com>
- */
- @Component
- export default class Tetris1 extends Vue {
- /** 实例化 view */
- view: TetrisView | null = null;
- /**
- * 页面挂载完成
- */
- mounted(): void {
- this.view = new TetrisView("tetris1");
- document.getElementById("tetris1")!!.focus();
- }
- /**
- * 重置操作
- */
- reset(): void {
- // 初始化游戏
- this.view!!.init();
- }
- }
- </script>
|