You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

#### 10 KiB Raw Blame History

title date categories aliases
Terminal-based Tetris - Part 1: Procedural polyomino generation 2020-01-18T08:30:00-07:00 [tutorial] [/post/textris-1/]

This is the first part of a series of tutorials on creating a terminal-based Tetris clone with Go.

Code for this tutorial is available on GitLab.

``````go get code.rocketnine.space/tslocum/terminal-tetris-tutorial/part-1 # Download and install
~/go/bin/part-1 # Run
``````

For a complete implementation of a Tetris clone in Go, see netris.

# Disclaimer

Tetris is a registered trademark of the Tetris Holding, LLC.

Rocket Nine Labs is in no way affiliated with Tetris Holding, LLC.

## Minos

Game pieces are called "minos" because they are polyominos. This tutorial series will focus on the seven one-sided terominos, where each piece has four blocks.

``````         XX      X      X         X      XX     XX
XXXX     XX     XXX     XXX     XXX     XX       XX

I       O       T       J       L       S       Z
``````

The number of blocks a mino has is also known as its rank.

### Mino data model

Tetris is played on an X-Y grid, so we will store minos as slices of points.

``````<!          !>
<!          !>
<! 1,7      !>
<!          !>
<!   3,5    !>
<!          !>
<!          !>
<!     5,2  !>
<!          !>
<!  2,0     !>
<!==========!>
\/\/\/\/\/
``````

Example coordinate positions in 10x10 playfield

{{< highlight go >}} // Point is an X,Y coordinate. type Point struct { X, Y int }

func (p Point) Rotate90() Point { return Point{p.Y, -p.X} } func (p Point) Rotate180() Point { return Point{-p.X, -p.Y} } func (p Point) Rotate270() Point { return Point{-p.Y, p.X} } func (p Point) Reflect() Point { return Point{-p.X, p.Y} }

// Mino is a set of Points. type Mino []Point

var exampleMino = Mino{{0, 0}, {1, 0}, {2, 0}, {1, 1}} // T piece {{< / highlight >}}

### Generating minos

Instead of hard-coding each piece into our game, let's procedurally generate them. This allows us to play with any size of mino.

### Sorting and comparing minos

To compare minos efficiently while generating, we will define a String method which sorts a mino's coordinates before printing them.

This will allow us to identify duplicate minos by comparing their string values.

{{< highlight go >}} func (m Mino) Len() int { return len(m) } func (m Mino) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func (m Mino) Less(i, j int) bool { return m[i].Y < m[j].Y || (m[i].Y == m[j].Y && m[i].X < m[j].X) }

func (m Mino) String() string { sort.Sort(m)

``````var b strings.Builder
b.Grow(5*len(m) + (len(m) - 1))

for i := range m {
if i > 0 {
b.WriteRune(',')
}

b.WriteRune('(')
b.WriteString(strconv.Itoa(m[i].X))
b.WriteRune(',')
b.WriteString(strconv.Itoa(m[i].Y))
b.WriteRune(')')
}

return b.String()
``````

}

// Render returns a visual representation of a Mino. func (m Mino) Render() string { var ( w, h = m.Size() c = Point{0, h - 1} b strings.Builder ) for y := h - 1; y >= 0; y-- { c.X = 0 c.Y = y

``````	for x := 0; x < w; x++ {
if !m.HasPoint(Point{x, y}) {
continue
}

for i := x - c.X; i > 0; i-- {
b.WriteRune(' ')
}

b.WriteRune('X')
c.X = x + 1
}

b.WriteRune('\n')
}

return b.String()
``````

} {{< / highlight >}}

Origin returns a translated mino located at `0,0` and with positive coordinates only.

A mino with the coordinates `(-3, -1), (-2, -1), (-1, -1), (-2, 0)` would be translated to `(0, 0), (1, 0), (2, 0), (1, 1)`:

``````    |                       |
|                       |X
--X-|-----      ->      ----XXX---
XXX|                       |
|                       |
``````

Translating a mino to `(0,0)`

{{< highlight go >}} // minCoords returns the lowest coordinate of a Mino. func (m Mino) minCoords() (int, int) { minx := m[0].X miny := m[0].Y

``````for _, p := range m[1:] {
if p.X < minx {
minx = p.X
}
if p.Y < miny {
miny = p.Y
}
}

return minx, miny
``````

}

// Origin returns a translated Mino located at 0,0 and with positive coordinates only. func (m Mino) Origin() Mino { minx, miny := m.minCoords()

``````newMino := make(Mino, len(m))
for i, p := range m {
newMino[i].X = p.X - minx
newMino[i].Y = p.Y - miny
}

return newMino
``````

} {{< / highlight >}}

Another transformation is applied not only to help identify duplicate minos, but also to retrieve their initial rotation, as pieces should spawn flat-side down.

``````XXX              X
X      ->      XXX
``````

Flattening a mino

Flatten calculates the flattest side of a mino and returns a flattened mino.

{{< highlight go >}} // Size returns the dimensions of a Mino. func (m Mino) Size() (int, int) { var x, y int for _, p := range m { if p.X > x { x = p.X } if p.Y > y { y = p.Y } }

``````return x + 1, y + 1
``````

}

// Flatten calculates the flattest side of a Mino and returns a flattened Mino. func (m Mino) Flatten() Mino { var ( w, h = m.Size() sides [4]int // Left Top Right Bottom ) for i := 0; i < len(m); i++ { if m[i].Y == 0 { sides[3]++ } else if m[i].Y == (h - 1) { sides[1]++ }

``````	if m[i].X == 0 {
sides[0]++
} else if m[i].X == (w - 1) {
sides[2]++
}
}

var (
largestSide   = 3
largestLength = sides[3]
)
for i, s := range sides[:2] {
if s > largestLength {
largestSide = i
largestLength = s
}
}

var rotateFunc func(Point) Point
switch largestSide {
case 0: // Left
rotateFunc = Point.Rotate270
case 1: // Top
rotateFunc = Point.Rotate180
case 2: // Right
rotateFunc = Point.Rotate90
default: // Bottom
return m
}

newMino := make(Mino, len(m))
for i := 0; i < len(m); i++ {
newMino[i] = rotateFunc(m[i])
}

return newMino
``````

} {{< / highlight >}}

Variations returns the three other rotations of a mino.

`````` X             X                 X
XXX    ->      XX      XXX      XX
X        X        X
``````

Variations of a mino

{{< highlight go >}} // Variations returns the three other rotations of a Mino. func (m Mino) Variations() []Mino { v := make([]Mino, 3) for i := 0; i < 3; i++ { v[i] = make(Mino, len(m)) }

``````for j := 0; j < len(m); j++ {
v[0][j] = m[j].Rotate90()
v[1][j] = m[j].Rotate180()
v[2][j] = m[j].Rotate270()
}

return v
``````

} {{< / highlight >}}

Canonical returns a flattened mino translated to `0,0`.

{{< highlight go >}} // Canonical returns a flattened Mino translated to 0,0. func (m Mino) Canonical() Mino { var ( ms = m.Origin().String() c = -1 v = m.Origin().Variations() vs string )

``````for i := 0; i < 3; i++ {
vs = v[i].Origin().String()
if vs < ms {
c = i
ms = vs
}
}

if c == -1 {
return m.Origin().Flatten().Origin()
}

return v[c].Origin().Flatten().Origin()
``````

} {{< / highlight >}}

Starting with a monomino (a mino with a single point: `0,0`), we will generate additional minos by adding neighboring points.

``````                              X                XX   X   X      X   XX  XX
X    ->     XX    ->    XXX  XX    ->    XXXX  XX  XXX  XXX  XXX  XX    XX
``````

Mino generation

Neighborhood returns the Von Neumann neighborhood of a point.

{{< highlight go >}} //Neighborhood returns the Von Neumann neighborhood of a Point. func (p Point) Neighborhood() []Point { return []Point{ {p.X - 1, p.Y}, {p.X, p.Y - 1}, {p.X + 1, p.Y}, {p.X, p.Y + 1}} } {{< / highlight >}}

NewPoints calculates the neighborhood of each point of a mino and returns only the new points.

{{< highlight go >}} // Neighborhood returns the Von Neumann neighborhood of a Point. func (m Mino) HasPoint(p Point) bool { for _, mp := range m { if mp == p { return true } }

``````return false
``````

}

// NewPoints calculates the neighborhood of each Point of a Mino and returns only the new Points. func (m Mino) NewPoints() []Point { var newPoints []Point

``````for _, p := range m {
for _, np := range p.Neighborhood() {
if m.HasPoint(np) {
continue
}

newPoints = append(newPoints, np)
}
}

return newPoints
``````

} {{< / highlight >}}

NewMinos returns a new mino for every new neighborhood point of a supplied mino.

{{< highlight go >}} // NewMinos returns a new Mino for every new neighborhood Point of a supplied Mino. func (m Mino) NewMinos() []Mino { points := m.NewPoints()

``````minos := make([]Mino, len(points))
for i, p := range points {
minos[i] = append(m, p).Canonical()
}

return minos
``````

} {{< / highlight >}}

### Generating unique minos

Generate procedurally generates minos of a supplied rank.

We generate minos for the rank below the requested rank and iterate over the variations of each mino, saving and returning all unique variations.

{{< highlight go >}} // monomino returns a single-block Mino. func monomino() Mino { return Mino{{0, 0}} }

// Generate procedurally generates Minos of a supplied rank. func Generate(rank int) ([]Mino, error) { switch { case rank < 0: return nil, errors.New("invalid rank") case rank == 0: return []Mino{}, nil case rank == 1: return []Mino{monomino()}, nil default: r, err := Generate(rank - 1) if err != nil { return nil, err }

``````	var (
minos []Mino
s     string
found = make(map[string]bool)
)
for _, mino := range r {
for _, newMino := range mino.NewMinos() {
s = newMino.Canonical().String()
if found[s] {
continue
}

minos = append(minos, newMino.Canonical())
found[s] = true
}
}

return minos, nil
}
``````

} {{< / highlight >}}

# Up next: The Matrix

In part two we create a Matrix to hold our Minos and implement SRS rotation.