Improve mino generation
This commit is contained in:
parent
833e3d7762
commit
bb8dd91895
11 changed files with 367 additions and 74 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,3 +2,5 @@
|
|||
dist/
|
||||
*.sh
|
||||
vendor/
|
||||
cmd/netris-server/netris-server
|
||||
cmd/netris/netris
|
||||
|
|
|
@ -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
38
pkg/mino/bag.go
Normal 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
45
pkg/mino/bag_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
46
pkg/mino/generate_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
194
pkg/mino/mino.go
194
pkg/mino/mino.go
|
@ -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
8
pkg/mino/mino_test.go
Normal 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
13
pkg/mino/minos.go
Normal 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
|
||||
}
|
|
@ -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}}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue