package tile import ( "strings" "testing" "salty/terrain/internal/field" "salty/terrain/internal/world" ) func testPlanet(t *testing.T) world.Planet { t.Helper() // 64 geology columns of 8 m is a 512 m circumference, 40 painted rows, 4 of polar pad. p := world.Planet{CellM: 8, W: 64, H: 48, PadY: 4, NoisePeriodM: 512} if err := p.Validate(); err != nil { t.Fatal(err) } return p } func TestNewGridDivides(t *testing.T) { p := testPlanet(t) // 64 detail cells at factor 4 is 16 geology cells, and 64 divides by 16. g, err := NewGrid(p, 4, 64, 8) if err != nil { t.Fatal(err) } if g.NX != 4 { t.Errorf("NX = %d, want 4", g.NX) } // 40 painted rows over 16-cell tiles is 3 rows, the last one short. if g.NY != 3 { t.Errorf("NY = %d, want 3", g.NY) } if g.MarginGeo != 2 { t.Errorf("MarginGeo = %d, want 2 (8 detail cells at factor 4)", g.MarginGeo) } } // X wraps, so a tile grid that did not come out whole would leave the last tile overlapping the first by an // arbitrary amount and there would be no honest way to name the seam. func TestNewGridRefusesASideThatDoesNotDivide(t *testing.T) { p := testPlanet(t) _, err := NewGrid(p, 4, 4*13, 8) // 13 geology cells does not divide 64 if err == nil { t.Fatal("accepted a tile side that does not divide the circumference") } if !strings.Contains(err.Error(), "X wraps") { t.Errorf("error %q does not say why", err) } if _, err := NewGrid(p, 4, 66, 8); err == nil { t.Fatal("accepted a tile side that is not a whole number of geology cells") } } // The last row is short rather than padded: padding would put invented ground in the output. func TestTilesCoverThePaintedRowsExactly(t *testing.T) { p := testPlanet(t) g, err := NewGrid(p, 4, 64, 8) if err != nil { t.Fatal(err) } seen := make([]int, p.W*p.PaintH()) for _, tl := range g.Tiles() { for y := tl.Y0; y < tl.Y0+tl.H; y++ { for x := tl.X0; x < tl.X0+tl.W; x++ { seen[y*p.W+x]++ } } } for i, n := range seen { if n != 1 { t.Fatalf("cell %d covered %d times, want exactly 1", i, n) } } last := g.Tiles()[len(g.Tiles())-1] if last.H != p.PaintH()-2*g.SideGeo { t.Errorf("the last tile row is %d cells, want %d", last.H, p.PaintH()-2*g.SideGeo) } } // A tile at the seam reads its left margin from the far side of the map, and one at a pole clamps rather // than wrapping: the top and bottom of the map are the poles, not each other. func TestCutWrapsInXAndClampsInY(t *testing.T) { p := testPlanet(t) g, err := NewGrid(p, 4, 64, 8) if err != nil { t.Fatal(err) } // A source whose value encodes its own position, so a misplaced read is obvious. src := field.New(p.W, p.PaintH(), p.CellM) for y := 0; y < src.H; y++ { for x := 0; x < src.W; x++ { src.Data[y*src.W+x] = float32(y*1000 + x) } } first := g.Tiles()[0] // X0 = 0, Y0 = 0: both edges out, ix, iy := g.Cut(first, src, 0) if ix != g.MarginGeo*g.Factor || iy != g.MarginGeo*g.Factor { t.Errorf("interior offset %d,%d, want %d", ix, iy, g.MarginGeo*g.Factor) } // The left margin is the far side of the cylinder. if got, want := out.Data[g.MarginGeo*out.W+0], float32(0*1000+p.W-g.MarginGeo); got != want { t.Errorf("left margin reads %v, want %v (the columns across the seam)", got, want) } // The top margin is row 0 repeated, not the bottom of the map. if got, want := out.Data[0*out.W+g.MarginGeo], float32(0*1000+0); got != want { t.Errorf("top margin reads %v, want %v (row 0 clamped)", got, want) } // And the interior is itself. if got, want := out.Data[g.MarginGeo*out.W+g.MarginGeo], float32(0); got != want { t.Errorf("interior corner reads %v, want %v", got, want) } } // The frame is what every hash and noise lattice in the detail passes is keyed on, so it has to name the // right physical place at detail resolution. func TestFrameIsTheDetailWorldPosition(t *testing.T) { p := testPlanet(t) g, err := NewGrid(p, 4, 64, 8) if err != nil { t.Fatal(err) } tiles := g.Tiles() second := tiles[1] // X0 = 16 geology cells f := g.Frame(second) if f.P.CellM != 2 { t.Errorf("frame cell = %v m, want 2", f.P.CellM) } if f.P.W != p.W*4 { t.Errorf("frame planet width = %d, want %d detail columns", f.P.W, p.W*4) } // The cut starts a margin before the interior: (16 - 2) geology cells is 56 detail columns. if f.X0 != (16-g.MarginGeo)*4 { t.Errorf("frame X0 = %d, want %d", f.X0, (16-g.MarginGeo)*4) } // The interior's first detail column is the margin in, and it must name geology column 16. wx, _ := f.PlanetXY(g.MarginGeo*g.Factor, 0) if wx != 16*4 { t.Errorf("the interior's first column is detail column %d, want %d", wx, 16*4) } if got, want := g.OriginXM(second), 16*8.0; got != want { t.Errorf("OriginXM = %v, want %v", got, want) } // Row 0 of the painted map is world Y zero; the polar pad is behind it. if got := g.OriginYM(tiles[0]); got != 0 { t.Errorf("OriginYM of the first tile row = %v, want 0", got) } } // A frame that straddles the seam names the same physical columns as one that does not. func TestASeamTileNamesTheSameColumns(t *testing.T) { p := testPlanet(t) g, err := NewGrid(p, 4, 64, 8) if err != nil { t.Fatal(err) } first := g.Tiles()[0] // X0 = 0, so its left margin is across the seam f := g.Frame(first) // Detail column 0 of the cut is (0 - margin) geology cells, wrapped. wx, _ := f.PlanetXY(0, 0) if want := (p.W - g.MarginGeo) * 4; wx != want { t.Errorf("the cut's first column is detail column %d, want %d across the seam", wx, want) } // And the interior's first column is detail column 0. wx, _ = f.PlanetXY(g.MarginGeo*g.Factor, 0) if wx != 0 { t.Errorf("the interior's first column is %d, want 0", wx) } }