Improve mino generation

This commit is contained in:
Trevor Slocum 2019-09-13 08:35:12 -07:00
parent 833e3d7762
commit bb8dd91895
11 changed files with 367 additions and 74 deletions

2
.gitignore vendored
View file

@ -2,3 +2,5 @@
dist/
*.sh
vendor/
cmd/netris-server/netris-server
cmd/netris/netris

View file

@ -5,7 +5,6 @@ import (
"log"
"strconv"
"git.sr.ht/~tslocum/netris/pkg/matrix"
"git.sr.ht/~tslocum/netris/pkg/mino"
)
@ -17,17 +16,15 @@ func init() {
func main() {
flag.Parse()
m := matrix.NewMatrix(10, 20, 20)
m.M[m.I(0, 2)] = mino.Solid
m.M[m.I(4, 5)] = mino.Garbage
//m.Print()
i, err := strconv.Atoi(flag.Arg(0))
if err != nil {
panic(err)
}
minos := mino.Generate(i)
minos, err := mino.Generate(i)
if err != nil {
panic(err)
}
for _, m := range minos {
log.Println(m.Render())
log.Println()

Binary file not shown.

38
pkg/mino/bag.go Normal file
View file

@ -0,0 +1,38 @@
package mino
import (
"math/rand"
"sync"
)
type Bag struct {
Minos []Mino
Original []Mino
sync.Mutex
}
func NewBag(minos []Mino) *Bag {
b := &Bag{Original: minos}
b.Shuffle()
return b
}
func (b *Bag) Take() Mino {
b.Lock()
defer b.Unlock()
mino := b.Minos[0]
if len(b.Minos) == 1 {
b.Shuffle()
} else {
b.Minos = b.Minos[1:]
}
return mino
}
func (b *Bag) Shuffle() {
b.Minos = b.Original
rand.Shuffle(len(b.Minos), func(i, j int) { b.Minos[i], b.Minos[j] = b.Minos[j], b.Minos[i] })
}

45
pkg/mino/bag_test.go Normal file
View file

@ -0,0 +1,45 @@
package mino
import (
"testing"
)
func TestBag(t *testing.T) {
var (
minos []Mino
err error
)
for _, d := range minoTestData {
minos, err = Generate(d.Rank)
if err != nil {
t.Errorf("failed to generate minos: %s", err)
}
if len(minos) != d.Minos {
t.Error("failed to generate minos: unexpected number of minos generated")
}
minos, err := Generate(d.Rank)
if err != nil {
t.Errorf("failed to create minos for bag: %s", err)
}
b := NewBag(minos)
taken := make(map[string]int)
for i := 1; i < 4; i++ {
for i := 0; i < d.Minos; i++ {
mino := b.Take()
taken[mino.String()]++
}
if len(taken) != d.Minos {
t.Errorf("minos placed in bag do not match minos taken - placed: %s - taken: %v", b.Minos, taken)
}
for _, mino := range minos {
if taken[mino.String()] != i {
t.Fatalf("minos placed in bag do not match minos taken - placed: %s - taken: %v", minos, taken)
}
}
}
}
}

View file

@ -1,24 +1,64 @@
package mino
import (
"errors"
"sync"
)
type MinoCache struct {
m map[int][]Mino
sync.RWMutex
}
func getCachedMinos(rank int) ([]Mino, bool) {
cachedMinos.RLock()
defer cachedMinos.RUnlock()
minos, ok := cachedMinos.m[rank]
return minos, ok
}
func resetCachedMinos() {
cachedMinos = &MinoCache{m: make(map[int][]Mino)}
}
var cachedMinos = &MinoCache{m: make(map[int][]Mino)}
// Generate
func Generate(n int) []Mino {
func Generate(rank int) ([]Mino, error) {
if minos, ok := getCachedMinos(rank); ok {
return minos, nil
}
switch {
case n < 0:
panic("invalid rank")
case n == 0:
return []Mino{}
case n == 1:
return []Mino{monomino()}
case rank < 0:
return nil, errors.New("invalid rank")
case rank == 0:
return []Mino{}, nil
case rank == 1:
return []Mino{monomino()}, nil
default:
r := Generate(n - 1)
r, err := Generate(rank - 1)
if err != nil {
return nil, err
}
var minos []Mino
found := make(map[string]bool)
for _, mino := range r {
for _, newMino := range mino.newMinos() {
minos = append(minos, newMino.translateToOrigin())
if s := newMino.String(); !found[s] {
minos = append(minos, newMino.translateToOrigin())
found[s] = true
}
}
}
return minos
cachedMinos.Lock()
cachedMinos.m[rank] = minos
cachedMinos.Unlock()
return minos, nil
}
}

46
pkg/mino/generate_test.go Normal file
View file

@ -0,0 +1,46 @@
package mino
import "testing"
func TestGenerate(t *testing.T) {
var (
minos []Mino
err error
)
for _, d := range minoTestData {
minos, err = Generate(d.Rank)
if err != nil {
t.Errorf("failed to generate minos for rank %d: %s", d.Rank, err)
}
if len(minos) != d.Minos {
t.Errorf("failed to generate minos for rank %d: expected to generate %d minos, got %d", d.Rank, d.Minos, len(minos))
}
}
}
func BenchmarkGenerate(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
var (
minos []Mino
err error
)
for n := 0; n < b.N; n++ {
b.StopTimer()
resetCachedMinos()
b.StartTimer()
for _, d := range minoTestData {
minos, err = Generate(d.Rank)
if err != nil {
b.Errorf("failed to generate minos: %s", err)
}
if len(minos) != d.Minos {
b.Errorf("failed to generate minos for rank %d: expected to generate %d minos, got %d", d.Rank, d.Minos, len(minos))
}
}
}
}

View file

@ -2,21 +2,43 @@ package mino
import (
"sort"
"strconv"
"strings"
)
type Mino []Point
func (m Mino) String() string {
var s strings.Builder
for i, p := range m {
if i > 0 {
s.WriteString(", ")
}
s.WriteString(p.String())
func (m Mino) Equal(other Mino) bool {
if len(m) != len(other) {
return false
}
return s.String()
for i := 0; i < len(m); i++ {
if !m.HasPoint(other[i]) {
return false
}
}
return true
}
func (m Mino) String() string {
sort.Sort(m)
var b strings.Builder
for i, p := range m.translateToOrigin() {
if i > 0 {
b.WriteRune(',')
}
b.WriteRune('(')
b.WriteString(strconv.Itoa(p.X))
b.WriteRune(',')
b.WriteString(strconv.Itoa(p.Y))
b.WriteRune(')')
}
return b.String()
}
func (m Mino) Len() int { return len(m) }
@ -25,27 +47,31 @@ 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) Width() int {
w := 0
func (m Mino) Size() (int, int) {
var x, y int
for _, p := range m {
if p.X > w {
w = p.X
if p.X > x {
x = p.X
}
if p.Y > y {
y = p.Y
}
}
return w
return x + 1, y + 1
}
func (m Mino) Render() string {
sort.Sort(m)
var b strings.Builder
b.WriteString(" ")
b.WriteRune(' ')
c := Point{0, 0}
for _, p := range m {
if p.Y > c.Y {
b.WriteString("\n ")
b.WriteRune('\n')
b.WriteRune(' ')
c.X = 0
}
if p.X > c.X {
@ -88,48 +114,113 @@ func (m Mino) minCoords() (int, int) {
func (m Mino) translateToOrigin() Mino {
minx, miny := m.minCoords()
newMino := make(Mino, len(m))
for i, p := range m {
newMino[i] = Point{p.X - minx, p.Y - miny}
m[i].X = p.X - minx
m[i].Y = p.Y - miny
}
sort.Sort(newMino)
return newMino
return m
}
func (m Mino) rotate(deg int) Mino {
var rotateFunc func(Point) Point
switch deg {
case 90:
rotateFunc = Point.Rotate90
case 180:
rotateFunc = Point.Rotate180
case 270:
rotateFunc = Point.Rotate270
default:
return m
}
for i := 0; i < len(m); i++ {
m[i] = rotateFunc(m[i])
}
return m
}
func (m Mino) variations() []Mino {
rr := make([]Mino, 8)
for i := 0; i < 8; i++ {
rr[i] = make(Mino, len(m))
v := make([]Mino, 3)
for i := 0; i < 3; i++ {
v[i] = make(Mino, len(m))
}
copy(rr[0], m)
for j := 0; j < len(m); j++ {
rr[1][j] = m[j].rotate90()
rr[2][j] = m[j].rotate180()
rr[3][j] = m[j].rotate270()
rr[4][j] = m[j].reflect()
rr[5][j] = m[j].rotate90().reflect()
rr[6][j] = m[j].rotate180().reflect()
rr[7][j] = m[j].rotate270().reflect()
v[0][j] = m[j].Rotate90()
v[1][j] = m[j].Rotate180()
v[2][j] = m[j].Rotate270()
}
return rr
return v
}
func (m Mino) canonical() Mino {
rr := m.variations()
minr := rr[0].translateToOrigin()
mins := minr.String()
for i := 1; i < 8; i++ {
r := rr[i].translateToOrigin()
s := r.String()
if s < mins {
minr = r
mins = s
var (
ms = m.String()
c = -1
v = m.variations()
vs string
)
for i := 0; i < 3; i++ {
vs = v[i].String()
if vs < ms {
c = i
ms = vs
}
}
return minr
if c == -1 {
return m.flatten()
}
return v[c].flatten()
}
func (m Mino) flatten() Mino {
w, h := m.Size()
var top, right, bottom, left int
for i := 0; i < len(m); i++ {
if m[i].Y == 0 {
top++
} else if m[i].Y == (h - 1) {
bottom++
}
if m[i].X == 0 {
left++
} else if m[i].X == (w - 1) {
right++
}
}
flattest := bottom
var rotate int
if left > flattest {
flattest = left
rotate = 90
}
if top > flattest {
flattest = top
rotate = 180
}
if right > flattest {
flattest = right
rotate = 270
}
if rotate > 0 {
m = m.rotate(rotate)
}
return m
}
func (m Mino) newPoints() Mino {
newMino := Mino{}
var newMino Mino
for _, p := range m {
n := p.Neighborhood()
for _, np := range n {
@ -138,17 +229,20 @@ func (m Mino) newPoints() Mino {
}
}
}
return newMino
}
func (m Mino) newMinos() []Mino {
pts := m.newPoints()
res := make([]Mino, len(pts))
for i, pt := range pts {
poly := make(Mino, len(m))
copy(poly, m)
poly = append(poly, pt)
res[i] = poly.canonical()
mino := make(Mino, len(m))
copy(mino, m)
points := m.newPoints()
minos := make([]Mino, len(points))
for i, p := range points {
minos[i] = append(mino, p).canonical()
}
return res
return minos
}

8
pkg/mino/mino_test.go Normal file
View file

@ -0,0 +1,8 @@
package mino
type MinoTestData struct {
Rank int
Minos int
}
var minoTestData = []*MinoTestData{{1, 1}, {2, 1}, {3, 2}, {4, 7}, {5, 18}, {6, 60}, {7, 196}}

13
pkg/mino/minos.go Normal file
View file

@ -0,0 +1,13 @@
package mino
type Minos []Mino
func (ms Minos) Has(m Mino) bool {
for _, msm := range ms {
if msm.Equal(m) {
return true
}
}
return false
}

View file

@ -1,25 +1,35 @@
package mino
import "fmt"
import (
"strconv"
"strings"
)
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} }
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} }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
var b strings.Builder
b.WriteRune('(')
b.WriteString(strconv.Itoa(p.X))
b.WriteRune(',')
b.WriteString(strconv.Itoa(p.Y))
b.WriteRune(')')
return b.String()
}
// Neighborhood returns the Von Neumann neighborhood of a point
func (p Point) Neighborhood() Mino {
return Mino{
{p.X - 1, p.Y},
{p.X + 1, p.Y},
{p.X, p.Y - 1},
{p.X + 1, p.Y},
{p.X, p.Y + 1}}
}