MoonBit支持实时可视化编程,唤起你的童年记忆「俄罗斯方块」

在数字化世界中,编程一直扮演着创新和想象力的重要角色。而当我们回首童年,有一款游戏可能在编程实践中发挥出意想不到的影响------那就是经典的"俄罗斯方块"。这款由俄罗斯人阿列克谢·帕基特诺夫(Алексей Пажитнов 英文:Alexey Pazhitnov)发明的小游戏,在1980年末期至1990年代初期风靡全世界。

借助 MoonBit,我们可以将这些宝贵的童年回忆融入到编程项目中。我们有机会创建个性化的俄罗斯方块游戏,并且学习如何设计和实现游戏逻辑、图形界面以及用户交互。更为重要的是,由于 MoonBit 支持实时可视化开发,你可以在开发过程中即时看到你的代码如何影响游戏的表现。这里通过视频展示,你可以实时更改代码来调整俄罗斯游戏的速度与背景颜色。

MoonBit支持实时可视化开发,助力你高效编程

现在,让我们把焦点转向编程实践中的童年记忆---俄罗斯方块。最近,一位MoonBit用户Luoxuwei在GitHub上分享了自己用MoonBit实现了俄罗斯方块的代码。今天我们就以他分享的代码为例,具体分享一下如何用MoonBit来编写俄罗斯方块。

01 什么是俄罗斯方块

俄罗斯方块原名是俄语Тетрис(英语是Tetris),这个名字来源于希腊语tetra,意思是"四",而游戏的作者最喜欢网球(tennis)。于是,他把两个词tetra和tennis合而为一,命名为Tetris,这也就是俄罗斯方块名字的由来。

俄罗斯方块的游戏规则很简单:由小方块组成的不同形状的板块陆续从屏幕上方落下来,玩家通过调整板块的位置和方向,使它们在屏幕底部拼出完整的一条或几条。这些完整的横条会随即消失,给新落下来的板块腾出空间,与此同时,玩家得到分数奖励。没有被消除掉的方块不断堆积起来,一旦堆到屏幕顶端,玩家便告输,游戏结束。

02用 MoonBit 编写俄罗斯方块

接下来,我们将分享如何用 MoonBit 来编写俄罗斯方块?

储存整个游戏的状态

首先,我们需要使用 struct Tetris 来储存整个游戏的状态:

sql 复制代码
struct Tetris {
  mut dead:Bool
  mut grid:List[Array[Int]]
  mut piece_pool:List[PIECE]
  mut current:PIECE
  mut piece_x:Int
  mut piece_y:Int
  mut piece_shap:Array[Array[Int]]
  mut score:Int
  mut row_completed:Int
}

grid 用来保存一个画面里面每个块的颜色,比如:

0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
1 1 1 1 0 0 0 0 0 0 0

可以用来表示下图:

生成俄罗斯方块

第一步:使用 generate_piece 生成俄罗斯方块

swift 复制代码
pub func generate_piece(self:Tetris) -> Bool {
  self.current = self.get_next_piece(true)
  self.piece_shap = self.current.piece_shap()
  self.piece_x = grid_col_count/2 - self.piece_shap[0].length()/2
  self.piece_y = 0
  return check_collision(self.grid, self.piece_shap, (self.piece_x, self.piece_y))
}

先通过 get_next_piece() 来获取下一个方块,就是从 piece_pool 中取出下一个 piece,这里获取的只是一个枚举类型。

swift 复制代码
pub func get_next_piece(self:Tetris, pop:Bool) -> PIECE {
  if self.piece_pool.length() == 0 {
    self.generate_piece_pool()
  }

  let Cons(cur, n) = self.piece_pool
  if pop {
    self.piece_pool = n 
  }
  cur
} 

第二步:通过 piece_shape 来获取具体的形状。俄罗斯方块每种类型的方块表示是用一个二维数组来表示,数组里的值是颜色的索引:

lua 复制代码
pub func piece_shape(self:PIECE) -> Array[Array[Int]] {
  match self {
    I => [[1, 1, 1, 1]]
    L => [[0, 0, 2],
          [2, 2, 2]]
    J => [[3, 0, 0],
          [3, 3, 3]]
    S => [[0, 4, 4],
          [4, 4, 0]]
    Z => [[5, 5, 0],
          [0, 5, 5]]
    T => [[6, 6, 6],
          [0, 6, 0]]
    O => [[7, 7],
          [7, 7]]
  }
} 

比如 L 表示的就是 L 形的图形, 如下图:

第三步:计算 piece 的 x 坐标和 y 坐标

第四步:调用 check_collision 检查是否有冲突

控制俄罗斯方块

我们通过 step 这个函数来移动和旋转方块,根据 action 的值来进行不同的操作:

scss 复制代码
pub func step(tetris:Tetris, action:Int) {
  if tetris.dead {
    return
  }
    
  match action {
    //move left
    1 => tetris.move_piece(-1)
    //move right
    2 => tetris.move_piece(1)
    //rotate
    3 => tetris.rotate_piece()
    //instant
    4 => tetris.drop_piece(true)
    _ => ()
  }
  tetris.drop_piece(false)
}

移动俄罗斯方块:

swift 复制代码
pub func move_piece(self:Tetris, delta:Int) {
  var new_x = self.pice_x + delta
  new_x = max(0, min(new_x, (grid_col_count - self.pice_shap[0].length())))
  if check_collision(self.grid, self.pice_shap, (new_x, self.pice_y)) {
    return
  }
  self.pice_x = new_x
}

旋转俄罗斯方块:

ini 复制代码
pub func rotate_piece(self:Tetris) {
  let r = self.pice_shape.length()
  let c = self.pice_shape[0].length()
  let new_shape = Array::make(c, Array::make(r, 0))
  var i = 0
  while i<c {
    new_shape[i] = Array::make(r, 0)
    i = i+1
  }
  
  var i_c = 0
  while i_c < c {
    var i_r = 0
    while i_r < r {
      new_shape[i_c][i_r] = self.pice_shape[r-i_r-1][i_c]
      i_r = i_r + 1
    }
    i_c = i_c + 1
  }
  var new_x = self.pice_x
  if (new_x + new_shape[0].length()) > grid_col_count {
    new_x = grid_col_count - new_shap[0].length()
  }

  if check_collision(self.grid, new_shape, (new_x, self.pice_y)) {
    return
  }
  self.piece_x = new_x
  self.piece_shape = new_shape
}

掉落俄罗斯方块:

swift 复制代码
pub func drop_piece(self:Tetris, instant:Bool) {
  if instant {
    let y = get_effective_height(self.grid, self.pice_shape, (self.piece_x, self.piece_y))
    self.piece_y = y + 1
  } else {
    self.piece_y = self.piece_y + 1
  }

  if instant == false && check_collision(self.grid, self.pice_shape, (self.piece_x, self.piece_y)) == false {
    return
  } 

  self.on_piece_collision()
}

这里的instant 参数用来判断是否是快速掉落方块;使用 on_piece_collison() 查找完整的行。然后消除他们。

消除方块

当一行满的时候方块需要被消除,我们通过 on_piece_collision 来完成消除。

先将这个方块添加进去:

scss 复制代码
pub func on_piece_collision(self:Tetris) {
  // ...

  //Add the current shap to grid
  fn go1(l:List[Array[Int]], r:Int) {
    match l {
      Cons(v, n) => {
        if r < y {
          return go1(n, r + 1)
        }

        if r >= (y + len_r) {
          return
        }
        var c = 0
        while c < len_c {
          if self.pice_shap[r - y][c] == 0 {
            c = c + 1
            continue
          }
          v[c + self.piece_x] = self.piece_shape[r - y][c]
          c = c + 1
        }
        return go1(n, r + 1)
      }
      Nil => ()
    }
  }
  go1(self.grid, 0)
}

消除已经填满的行:

scss 复制代码
pub func on_piece_collision(self : Tetris) {
  //...

  //Delete the complete row
  self.row_completed = 0
  fn go2(l:List[Array[Int]]) -> List[Array[Int]] {
    match l {
      Nil => Nil
      Cons(v, n) => {
        if contain(v, 0) {
          return Cons(v, go2(n))
        } else {
          self.row_completed = self.row_completed + 1
          return go2(n)
        }
      }
    }
  }
  var new_grid:List[Array[Int]] = Nil
  new_grid = go2(self.grid)
}

使用 Moonbit External Ref 画图

根据 Tetris 中保存的信息调用 Canvas 去画图:

scss 复制代码
pub func draw(canvas : Canvas_ctx, tetris : Tetris) {
    var c = 0

    //draw backgroud
    while c < grid_col_count {
      let color = if (c%2) == 0 {0} else {1}
      canvas.set_fill_style(color)
      canvas.fill_rect(c, 0, 1, grid_row_count)
      c = c + 1
    }

    draw_piece(canvas, tetris.grid, (0, 0))
    draw_piece(canvas, tetris.piece_shape.stream(), (tetris.piece_x, tetris.piece_y))

    if tetris.dead {
      canvas.draw_game_over()
    }
}


func draw_piece(canvas:Canvas_ctx, matrix:List[Array[Int]], offset:(Int, Int)) {

    fn go(l:List[Array[Int]], r:Int, canvas:Canvas_ctx) {
      match l {
        Cons(v, n) => {
          var c = 0
          while c < v.length() {
            if v[c] == 0 {
              c = c+1
              continue
            }
            canvas.set_fill_style(v[c]+1)
            canvas.fill_rect(offset.0 + c, offset.1 + r, 1, 1)
            canvas.set_stroke_color(0)
            canvas.set_line_width(0.1)
            canvas.stroke_rect(offset.0 + c, offset.1 + r, 1, 1)
            c = c + 1
          }
          go(n, r+1, canvas)
        }
        Nil => ()
      }
    }
    go(matrix, 0, canvas)
}

JavaScript 监听和渲染

添加对键盘事件的监听:

javascript 复制代码
 window.addEventListener("keydown", (e) => {
  if (!requestAnimationFrameId) return
  switch (e.key) {
    case "ArrowLeft": {
        tetris_step(tetris, 1)
        break
    }
    case "ArrowRight": {
        tetris_step(tetris, 2)
        break
    }
    case "ArrowDown": {
        tetris_step(tetris, 4)
        break
    }
    case "ArrowUp": {
        tetris_step(tetris, 3)
        break
    }
  }
}) 

更新画面,这里会调用使用 MoonBit 写的draw(tetris_draw)函数:

javascript 复制代码
function update(time = 0) {
  const deltaTime = time - lastTime
  dropCounter += deltaTime
  if (dropCounter > dropInterval) {
    tetris_step(tetris, 0);
    scoreDom.innerHTML = "score: " + tetris_score(tetris)
    dropCounter = 0
  }
  lastTime = time
  tetris_draw(context, tetris);
  requestAnimationFrameId = requestAnimationFrame(update)
}

完整的代码:github.com/moonbitlang...

如果你想要看视频版的教程,可以点击下方视频观看

MoonBit编程实践|如何用400行MoonBit代码还原经典的俄罗斯方块?

当然,大家如果想体验一下实时coding调试俄罗斯方块,可以直接访问我们在线IDE:www.moonbitlang.cn/gallery/tet...

相关推荐
肖哥弹架构3 小时前
Vue组件开发:从入门到架构师
前端·vue.js·程序员
我是陈泽1 天前
一行 Python 代码能实现什么丧心病狂的功能?圣诞树源代码
开发语言·python·程序员·编程·python教程·python学习·python教学
肖哥弹架构2 天前
Spring 全家桶使用教程
java·后端·程序员
IT杨秀才5 天前
自己动手写了一个协程池
后端·程序员·go
程序员麻辣烫7 天前
像AI一样思考
程序员
一颗苹果OMG8 天前
关于进游戏公司实习的第一周
前端·程序员
万少9 天前
你会了吗 HarmonyOS Next 项目级别的注释规范
前端·程序员·harmonyos
楽码9 天前
彻底理解时间?在编程中使用原子钟
后端·算法·程序员
江南一点雨10 天前
又一家培训机构即将倒闭!打工人讨薪无果,想报名的小伙伴擦亮眼睛~
java·程序员
用户861782773651810 天前
ELK 搭建 & 日志集成
java·后端·程序员