2 changed files with 362 additions and 1 deletions
@ -0,0 +1,361 @@
@@ -0,0 +1,361 @@
|
||||
--- |
||||
title: "Textris - Creating a terminal-based Tetris clone in Go - Part 1: Pieces and playfield" |
||||
date: 2019-11-08T01:42:18-07:00 |
||||
categories: [tutorial] |
||||
draft: true |
||||
--- |
||||
|
||||
This is the first in a series of tutorials on creating a [terminal-based](https://en.wikipedia.org/wiki/Text-based_user_interface) [Tetris](https://en.wikipedia.org/wiki/Tetris) clone with [Go](https://golang.org). |
||||
Game pieces (minos) are generated and added to a playfield (matrix). |
||||
|
||||
## Contents |
||||
|
||||
* [Minos](#minos) |
||||
* [Data model](#data-model) |
||||
* [Generation](#generation) |
||||
* [Matrix](#matrix) |
||||
* [Data model](#data-model) |
||||
|
||||
# Minos |
||||
|
||||
Game pieces are called "Minos" because they are [polyominos](https://en.wikipedia.org/wiki/Polyomino). |
||||
Tetris is normally played using the seven one-sided [terominos](https://en.wikipedia.org/wiki/Tetromino): |
||||
|
||||
|
||||
I |
||||
|
||||
████ |
||||
|
||||
|
||||
O |
||||
|
||||
██ |
||||
██ |
||||
|
||||
|
||||
T |
||||
|
||||
█ |
||||
███ |
||||
|
||||
|
||||
J |
||||
|
||||
█ |
||||
███ |
||||
|
||||
|
||||
L |
||||
|
||||
█ |
||||
███ |
||||
|
||||
|
||||
S |
||||
|
||||
██ |
||||
██ |
||||
|
||||
|
||||
Z |
||||
|
||||
██ |
||||
██ |
||||
|
||||
## Data model |
||||
|
||||
Tetris is played on an [X-Y grid](https://en.wikipedia.org/wiki/Cartesian_coordinate_system). |
||||
We will store Minos as slices of points (coordinates): |
||||
|
||||
```go |
||||
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} } |
||||
|
||||
type Mino []Point |
||||
|
||||
minoT := Mino{{0, 0}, {1, 0}, {2, 0}, {1, 1}} |
||||
``` |
||||
|
||||
## Generation |
||||
|
||||
We could hard-code each piece into our game. Or, we could procedurally generate them, allowing us to play with any rank (size) of mino. |
||||
|
||||
To compare the minos more easily, we will use their string representation. |
||||
We sort the coordinates before printing so we can identify duplicate minos. |
||||
|
||||
```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() |
||||
} |
||||
``` |
||||
|
||||
Origin returns a translated mino located at 0,0 and with positive coordinates only. |
||||
This is also to identify duplicate minos. |
||||
|
||||
```go |
||||
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 |
||||
} |
||||
|
||||
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 |
||||
} |
||||
``` |
||||
|
||||
Another transformation is applied not only to identify duplicate minos, but also to retrieve their initial rotation, as [pieces spawn flat-side down](https://tetris.wiki/Super_Rotation_System). |
||||
The flattest side is calculated and a rotated mino is returned. |
||||
|
||||
```go |
||||
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)) |
||||
copy(newMino, m) |
||||
for i := 0; i < len(m); i++ { |
||||
newMino[i] = rotateFunc(newMino[i]) |
||||
} |
||||
|
||||
return newMino |
||||
} |
||||
``` |
||||
|
||||
Variations returns all other rotations of a mino. |
||||
|
||||
```go |
||||
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 |
||||
} |
||||
``` |
||||
|
||||
Canonical returns a flattened mino translated to 0,0. |
||||
|
||||
```go |
||||
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() |
||||
} |
||||
``` |
||||
|
||||
WIP |
||||
|
||||
```go |
||||
func (m Mino) HasPoint(p Point) bool { |
||||
for _, mp := range m { |
||||
if mp == p { |
||||
return true |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
func (m Mino) newPoints() Mino { |
||||
var newMino Mino |
||||
|
||||
for _, p := range m { |
||||
for _, np := range p.Neighborhood() { |
||||
if !m.HasPoint(np) { |
||||
newMino = append(newMino, np) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return newMino |
||||
} |
||||
|
||||
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 |
||||
} |
||||
``` |
||||
|
||||
# Matrix |
||||
|
||||
The matrix is typically 10 blocks wide and 20 blocks high. |
||||
|
||||
## Data model |
||||
|
||||
A block represents the contents of a single X-Y coordinate on the matrix. |
||||
|
||||
```go |
||||
type Block int |
||||
|
||||
const ( |
||||
BlockNone Block = iota |
||||
BlockSolidBlue |
||||
BlockSolidCyan |
||||
BlockSolidRed |
||||
BlockSolidYellow |
||||
BlockSolidMagenta |
||||
BlockSolidGreen |
||||
BlockSolidOrange |
||||
) |
||||
``` |
||||
|
||||
The matrix will be stored as a slice of blocks. |
||||
The zero-value of Block is a blank space. |
||||
|
||||
```go |
||||
type Matrix struct { |
||||
W int // Width |
||||
H in // Height |
||||
B int // Buffer height |
||||
|
||||
M []Block // Contents |
||||
} |
||||
|
||||
func NewMatrix(w int, h int, b int) *Matrix { |
||||
m := Matrix{ |
||||
W: w, |
||||
H: h, |
||||
B: b, |
||||
M: make([]Block, w*(h+b)), |
||||
} |
||||
|
||||
return &m |
||||
} |
||||
``` |
||||
|
||||
To retrieve the contents of a point, we calculate its index by multiplying the Y coordinate with the matrix width and adding the X coordinate. |
||||
|
||||
```go |
||||
func I(x int, y int, w int) int { |
||||
if x < 0 || x >= w || y < 0 { |
||||
log.Panicf("failed to retrieve index for %d,%d width %d: invalid coordinates", x, y, w) |
||||
} |
||||
|
||||
return (y * w) + x |
||||
} |
||||
|
||||
func (m *Matrix) Block(x int, y int) Block { |
||||
if y >= m.H+m.B { |
||||
log.Panicf("failed to retrieve block at %d,%d: invalid y coordinate", x, y) |
||||
} |
||||
|
||||
index := I(x, y, m.W) |
||||
return m.M[index] |
||||
} |
||||
``` |
Loading…
Reference in new issue