Browse Source

Add Tetris-1 draft

master
Trevor Slocum 3 years ago
parent
commit
a4fd5f5a35
  1. 361
      content/post/tetris-1.md
  2. 2
      content/post/tview-and-you.md

361
content/post/tetris-1.md

@ -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]
}
```

2
content/post/tview-and-you.md

@ -6,7 +6,7 @@ categories: [tutorial] @@ -6,7 +6,7 @@ categories: [tutorial]
<a href="https://github.com/rivo/tview/tree/master/demos/presentation"><img src="https://raw.githubusercontent.com/rivo/tview/master/tview.gif" width=640" height="446" border="0" title="tview presentation demo"></a>
This is an introduction to using [tview](https://github.com/rivo/tview) to create rich terminal-based user interfaces with Go.
This is an introduction to using [tview](https://github.com/rivo/tview) to create rich terminal-based user interfaces with [Go](https://golang.org).
## Contents

Loading…
Cancel
Save