让我们在 MoonBit 上编写俄罗斯方块游戏!

在 MoonBit 上编写俄罗斯方块游戏! - 图1

在20世纪80年代和90年代长大的人们会记得俄罗斯方块。几十年来,这个游戏一直在挑战我们的手眼协调能力和快速做出决策的能力。俄罗斯方块并没有消失——它仍然存在,几乎可以在任何设备上玩。

上周,一位名叫 Luoxuwei 的 MoonBit 用户在 GitHub 上分享了他使用 MoonBit 实现的俄罗斯方块代码,提供了一个有趣的编程练习。今天,我们将利用 Luoxuwei 的代码作为一个实际例子,展示如何使用 MoonBit 编程俄罗斯方块。

什么是俄罗斯方块游戏?

首先,让我们深入探讨一下俄罗斯方块的本质。

俄罗斯方块是由俄罗斯人阿列克谢·帕基特诺夫(Алексей Пажитнов)发明的。这款游戏的原始名称是俄语中的“Тетрис”(发音为Tetris),这个词源于希腊语的“tetra”,意为“四”,以及阿列克谢对网球的热爱,这是他最喜欢的运动。因此,他将“tetra”和“tennis”这两个词结合起来,创造了“Tetris”这个名字,这就是游戏名字的由来。

俄罗斯方块的规则非常简单:由小方块组成的各种形状从屏幕顶部不断落下,玩家需要调整这些形状的位置和方向,以在屏幕底部形成完整的行。当行被完成后,完成的行会消失,为新的下落形状腾出空间,并为玩家赢得分数。如果方块没有被清除并继续堆叠到屏幕顶部,玩家就会失败,游戏结束。

如何在 MoonBit 上编写俄罗斯方块

使用 struct Tetris 来存储整个游戏状态:

  1. struct Tetris {
  2. mut dead:Bool
  3. mut grid:List[Array[Int]]
  4. mut piece_pool:List[PIECE]
  5. mut current:PIECE
  6. mut piece_x:Int
  7. mut piece_y:Int
  8. mut piece_shape:Array[Array[Int]]
  9. mut score:Int
  10. mut row_completed:Int
  11. }

使用 grid 来保存每一块颜色的信息

例如:

  1. 0 0 0 0 0 0 0 0 0 0 0
  2. 0 0 0 0 0 0 0 0 0 0 0
  3. 1 1 1 1 0 0 0 0 0 0 0

你可以用它们来表示以下的图片: 在 MoonBit 上编写俄罗斯方块游戏! - 图2

生成俄罗斯方块

使用 generate_piece 函数来生成俄罗斯方块

  1. pub func generate_piece(self:Tetris) -> Bool {
  2. self.current = self.get_next_piece(true)
  3. self.piece_shape = self.current.piece_shape()
  4. self.piece_x = grid_col_count/2 - self.piece_shape[0].length()/2
  5. self.piece_y = 0
  6. return check_collision(self.grid, self.piece_shape, (self.piece_x, self.piece_y))
  7. }

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

  1. pub func get_next_piece(self:Tetris, pop:Bool) -> PIECE {
  2. if self.piece_pool.length() == 0 {
  3. self.generate_piece_pool()
  4. }
  5. let Cons(cur, n) = self.piece_pool
  6. if pop {
  7. self.piece_pool = n
  8. }
  9. cur
  10. }

其次,利用 piece_shape 来访问特定的形状。每种类型的俄罗斯方块方块都是用二维数组来表示的,数组的值对应于颜色索引。

  1. pub func piece_shape(self:PIECE) -> Array[Array[Int]] {
  2. match self {
  3. I => [[1, 1, 1, 1]]
  4. L => [[0, 0, 2],
  5. [2, 2, 2]]
  6. J => [[3, 0, 0],
  7. [3, 3, 3]]
  8. S => [[0, 4, 4],
  9. [4, 4, 0]]
  10. Z => [[5, 5, 0],
  11. [0, 5, 5]]
  12. T => [[6, 6, 6],
  13. [0, 6, 0]]
  14. O => [[7, 7],
  15. [7, 7]]
  16. }
  17. }

例如,L 代表一个 L 形的图形,如下所示:

在 MoonBit 上编写俄罗斯方块游戏! - 图3

第三,计算方块的 x 坐标和 y 坐标。

第四,调用 check_collision 来确定是否存在碰撞。

控制俄罗斯方块

我们使用 step 函数来移动和旋转方块,根据 action 的值执行不同的操作。

  1. pub func step(tetris:Tetris, action:Int) {
  2. if tetris.dead {
  3. return
  4. }
  5. match action {
  6. //move left
  7. 1 => tetris.move_piece(-1)
  8. //move right
  9. 2 => tetris.move_piece(1)
  10. //rotate
  11. 3 => tetris.rotate_piece()
  12. //instant
  13. 4 => tetris.drop_piece(true)
  14. _ => ()
  15. }
  16. tetris.drop_piece(false)
  17. }
  1. 移动俄罗斯方块
  1. pub func move_piece(self:Tetris, delta:Int) {
  2. var new_x = self.piece_x + delta
  3. new_x = max(0, min(new_x, (grid_col_count - self.piece_shape[0].length())))
  4. if check_collision(self.grid, self.piece_shape, (new_x, self.piece_y)) {
  5. return
  6. }
  7. self.piece_x = new_x
  8. }
  1. 旋转俄罗斯方块
  1. pub func rotate_piece(self:Tetris) {
  2. let r = self.piece_shape.length()
  3. let c = self.piece_shape[0].length()
  4. let new_shape = Array::make(c, Array::make(r, 0))
  5. var i = 0
  6. while i<c {
  7. new_shape[i] = Array::make(r, 0)
  8. i = i+1
  9. }
  10. var i_c = 0
  11. while i_c < c {
  12. var i_r = 0
  13. while i_r < r {
  14. new_shape[i_c][i_r] = self.piece_shape[r-i_r-1][i_c]
  15. i_r = i_r + 1
  16. }
  17. i_c = i_c + 1
  18. }
  19. var new_x = self.piece_x
  20. if (new_x + new_shape[0].length()) > grid_col_count {
  21. new_x = grid_col_count - new_shape[0].length()
  22. }
  23. if check_collision(self.grid, new_shape, (new_x, self.piece_y)) {
  24. return
  25. }
  26. self.piece_x = new_x
  27. self.piece_shape = new_shape
  28. }
  1. 让俄罗斯方块方块掉落
  1. pub func drop_piece(self:Tetris, instant:Bool) {
  2. if instant {
  3. let y = get_effective_height(self.grid, self.piece_shape, (self.piece_x, self.piece_y))
  4. self.piece_y = y + 1
  5. } else {
  6. self.piece_y = self.piece_y + 1
  7. }
  8. if instant == false && check_collision(self.grid, self.piece_shape, (self.piece_x, self.piece_y)) == false {
  9. return
  10. }
  11. self.on_piece_collision()
  12. }

我们需要关注的是:

参数 instant 用于确定是否是一个快速下降的方块。

使用 on_piece_collision() 来找到完整的行。然后消除它们。

清除俄罗斯方块

当一行被完全占用时,需要清除这些方块。我们通过使用 on_piece_collision 函数来实现这一点。

首先,添加对应的块

  1. pub func on_piece_collision(self:Tetris) {
  2. // ...
  3. //Add the current shap to grid
  4. fn go1(l:List[Array[Int]], r:Int) {
  5. match l {
  6. Cons(v, n) => {
  7. if r < y {
  8. return go1(n, r + 1)
  9. }
  10. if r >= (y + len_r) {
  11. return
  12. }
  13. var c = 0
  14. while c < len_c {
  15. if self.piece_shape[r - y][c] == 0 {
  16. c = c + 1
  17. continue
  18. }
  19. v[c + self.piece_x] = self.piece_shape[r - y][c]
  20. c = c + 1
  21. }
  22. return go1(n, r + 1)
  23. }
  24. Nil => ()
  25. }
  26. }
  27. go1(self.grid, 0)
  28. }

删除被完全占用的行。

  1. pub func on_piece_collision(self : Tetris) {
  2. //...
  3. //Delete the complete row
  4. self.row_completed = 0
  5. fn go2(l:List[Array[Int]]) -> List[Array[Int]] {
  6. match l {
  7. Nil => Nil
  8. Cons(v, n) => {
  9. if contain(v, 0) {
  10. return Cons(v, go2(n))
  11. } else {
  12. self.row_completed = self.row_completed + 1
  13. return go2(n)
  14. }
  15. }
  16. }
  17. }
  18. var new_grid:List[Array[Int]] = Nil
  19. new_grid = go2(self.grid)
  20. }

使用 MoonBit 进行绘图(外部引用)

根据 Tetris 中存储的信息,使用 Canvas 进行绘制。

  1. pub func draw(canvas : Canvas_ctx, tetris : Tetris) {
  2. var c = 0
  3. //draw backgroud
  4. while c < grid_col_count {
  5. let color = if (c%2) == 0 {0} else {1}
  6. canvas.set_fill_style(color)
  7. canvas.fill_rect(c, 0, 1, grid_row_count)
  8. c = c + 1
  9. }
  10. draw_piece(canvas, tetris.grid, (0, 0))
  11. draw_piece(canvas, tetris.piece_shape.stream(), (tetris.piece_x, tetris.piece_y))
  12. if tetris.dead {
  13. canvas.draw_game_over()
  14. }
  15. }
  16. func draw_piece(canvas:Canvas_ctx, matrix:List[Array[Int]], offset:(Int, Int)) {
  17. fn go(l:List[Array[Int]], r:Int, canvas:Canvas_ctx) {
  18. match l {
  19. Cons(v, n) => {
  20. var c = 0
  21. while c < v.length() {
  22. if v[c] == 0 {
  23. c = c+1
  24. continue
  25. }
  26. canvas.set_fill_style(v[c]+1)
  27. canvas.fill_rect(offset.0 + c, offset.1 + r, 1, 1)
  28. canvas.set_stroke_color(0)
  29. canvas.set_line_width(0.1)
  30. canvas.stroke_rect(offset.0 + c, offset.1 + r, 1, 1)
  31. c = c + 1
  32. }
  33. go(n, r+1, canvas)
  34. }
  35. Nil => ()
  36. }
  37. }
  38. go(matrix, 0, canvas)
  39. }

JS 监听事件和渲染页面

监听键盘事件

  1. window.addEventListener("keydown", (e) => {
  2. if (!requestAnimationFrameId) return
  3. switch (e.key) {
  4. case "ArrowLeft": {
  5. tetris_step(tetris, 1)
  6. break
  7. }
  8. case "ArrowRight": {
  9. tetris_step(tetris, 2)
  10. break
  11. }
  12. case "ArrowDown": {
  13. tetris_step(tetris, 4)
  14. break
  15. }
  16. case "ArrowUp": {
  17. tetris_step(tetris, 3)
  18. break
  19. }
  20. }
  21. })

更新屏幕,这里使用 MoonBit 调用 draw 函数(称为 tetris_draw)。

  1. function update(time = 0) {
  2. const deltaTime = time - lastTime
  3. dropCounter += deltaTime
  4. if (dropCounter > dropInterval) {
  5. tetris_step(tetris, 0);
  6. scoreDom.innerHTML = "score: " + tetris_score(tetris)
  7. dropCounter = 0
  8. }
  9. lastTime = time
  10. tetris_draw(context, tetris);
  11. requestAnimationFrameId = requestAnimationFrame(update)
  12. }

完整的代码地址在:https://github.com/moonbitlang/moonbit-docs/tree/main/examples/tetris