写个TERTRIS

写个TERTRIS

用go写了个TETRIS

突发奇想闲来无事情想用go写一个Tertris(俄罗斯方块)玩

上网小小检索了一下,确实有很多可以实现游戏ui的2D引擎,但比较懒,不是很像搞明白怎么去用,所以选择了直接在终端打印出来。

既然想做,那就想想应该如何设计

先看看正版到底长啥样

Typical_Tetris_Game.svg

分析一下:

  1. 有个方方的游戏区域(显而易见)
  2. 方块会自然下落(这个嘛,没想好怎么处理就设计成直接放置的那种)
  3. 一行满了消去一行,然后下落

开始写吧

先定义一个游戏结构

1
2
3
4
5
6
7
8
9
10
11
12
type Game struct {
Board [][]int // 游戏区域
Active [][]int // 还没用上,原来打算用来保存可掉落信息的
Score int
Level int
}

const (
WIDTH = 15 // 区域大小啦
HEIGHT = 20
)

第一步当然是初始化了

1
2
3
4
5
6
7
func BoardInit(g *define.Game) {
// 初始化游戏板
g.Board = make([][]int, define.HEIGHT)
for i := range g.Board {
g.Board[i] = make([]int, define.WIDTH)
}
}

然后就是打印出游戏区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 打印游戏板
func PrintBoard(g *define.Game) {
// 这里是在每次出现变动时清除内容
cmd := exec.Command("cmd", "/c", "cls")
cmd.Stdout = os.Stdout
cmd.Run()
// ... ///
for i := range g.Board {
fmt.Print("|")
if i == 4 {
fmt.Print("------------------------------|\n|")
}
for j := range g.Board[i] {
if g.Board[i][j] == 0 { // 有方块的话就打印一个矩形
fmt.Print(" ")
} else {
fmt.Print("■ ")
}
}
fmt.Printf("|\n")
}
fmt.Print("|------------------------------|\n")
}

就可以看到像这样的输出

版面

然后就应该在上方形成方块了

因为只有7种基本形状,于是为决定直接定义7个基本形状

至于为什么定义为4x4,是因为在游戏中这些方块是可以旋转的,为了简单起见,决定统一定义成4x4的结构,在旋转中只需修改它们的位置就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
var Tetrominoes = [][][]int{
{
{0, 0, 0, 0},
{1, 1, 1, 1},
{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, 0, 1, 0},
{1, 1, 1, 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},
},
{
{0, 1, 1, 0},
{1, 1, 0, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},
},
{
{1, 1, 1, 0},
{0, 1, 0, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},
},
{
{1, 1, 0, 0},
{0, 1, 1, 0},
{0, 0, 0, 0},
{0, 0, 0, 0},
},
}

每次获取的方块都是随机的于是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func RandomTetromino() [][]int {
// 对随机数生成器进行种子随机化
rand.Seed(time.Now().UnixNano())
// 从Tetrominoes数组中随机选择一个形状
shape := define.Tetrominoes[rand.Intn(len(define.Tetrominoes))]

// 复制所选形状的矩阵到游戏板里
tetromino := make([][]int, len(shape))
for i := range shape {
tetromino[i] = make([]int, len(shape[i]))
copy(tetromino[i], shape[i])
}

return tetromino
}


func NewTetrominoIn(g *define.Game) {
// 生成新的俄罗斯方块
nextTetromino := RandomTetromino()
for i := range nextTetromino {
for j := range nextTetromino[i] {
if nextTetromino[i][j] == 1 {
g.Board[i][j] = 1
}
}
}
}

就可以打印出类似于这样的界面

例子

至于移动或旋转就很简单了,只需找到生成的方块再作相应处理就好了,这里就不贴出代码了。

下面处理消去一行的操作

其实就是每次遍历一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func ClearFullRows(g *define.Game) {
// ... //
flag := true

for y := define.HEIGHT - 1; y >= 0; y-- {
flag = true
for x := 0; x < define.WIDTH; x++ {
if g.Board[y][x] == 0 {
flag = false
break
}
}
if flag {

// 去除被消去的行
for x := 0; x < define.WIDTH; x++ {
g.Board[y][x] = 0
}

// ... //
}
}
}

这时候就有了个问题

如何下落?

  • 首先,相互连接的方块是不可以掉到底部的
  • 消去的不一定是下一行
  • 所有相连的方块是一个整体

那这有有点头疼了,首先就是如何找到掉落方块整体,然后如何确定掉落高度

那就先处理如何找到掉落方块整体区间

由于只需要确定最小下落值,所以只用确定方块的左右范围就好了,但如何区查找呢?

因为整体说明这些方块左右上下想连,所以可以使用DFS算法去查找!

这里格外传入了ij两个值代表开始检索的左下角,因为只有在清除行上的部分才会发生掉落,所以要传入坐标以确定起始位置

而传入的leftright就修改为得到的数值而继续使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func Dfs(grid [][]int, i int, j int, visited [][]bool, left *int, right *int) {
// 检查当前位置是否已经被访问过,或者是否为0
if i < 0 || i >= len(grid) ||
j < 0 || j >= len(grid[0]) ||
grid[i][j] == 0 || visited[i][j] {
return
}
// 将当前位置标记为已访问
visited[i][j] = true
// 更新左右边界
if *left > j {
*left = j
}
if *right < j {
*right = j
}
// 递归遍历相邻的位置
Dfs(grid, i-1, j, visited, left, right) // 上
Dfs(grid, i+1, j, visited, left, right) // 下
Dfs(grid, i, j-1, visited, left, right) // 左
Dfs(grid, i, j+1, visited, left, right) // 右
}

关于掉落块的掉落高度

emmm好像没啥好分析的,直接给出代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 从左往右查找,这个方法比较蠢
// startLine 代表的是被消去的那一行
// 详细代码在仓库里
func Drop(g *define.Game, startLine int) {
column := 0
for column < define.WIDTH {
// 左右开始是重合的
left, right := column, column
for y := startLine; y >= 5; y-- {
// 找到第一个方块,然后进行搜索
if g.Board[y][column] == 1 {
visited := make([][]bool, len(g.Board))
for i := range visited {
visited[i] = make([]bool, len(g.Board[i]))
}
// 通过Dfs查找范围
utils.Dfs(g.Board, y, column, visited, &left, &right)
break
}
}

var dropHeight int = 999
top := startLine

for i := left; i <= right; i++ {
for y := startLine; y >= 0; y-- {
if g.Board[y][i] == 1 {
top = y
break
}
}

for y := top + 1; y <= define.HEIGHT; y++ {
if y == define.HEIGHT || g.Board[y][i] == 1 {
if dropHeight > y-top-1 {
dropHeight = y - top - 1
}
break
}
}
}

// 获取了掉落高度,将范围内的下移
for i := left; i <= right; i++ {
for y := top; y >= 5; y-- {
g.Board[y+dropHeight][i] = g.Board[y][i]
g.Board[y][i] = 0
}
}
column = right + 1
}
}

运行例子,可能一定还有一堆bug,不过能跑就好

这是我的代码仓库,还有许多不足,望日后逐步修正

演示