Noah Petherbridge
5654145fd8
Finally add a second option for Chunk MapAccessor implementation besides the MapAccessor. The RLEAccessor is basically a MapAccessor that will compress your drawing with Run Length Encoding (RLE) in the on-disk format in the ZIP file. This slashes the file sizes of most levels: * Shapeshifter: 21.8 MB -> 8.1 MB * Jungle: 10.4 MB -> 4.1 MB * Zoo: 2.8 MB -> 1.3 MB Implementation details: * The RLE binary format for Chunks is a stream of Uvarint pairs storing the palette index number and the number of pixels to repeat it (along the Y,X axis of the chunk). * Null colors are represented by a Uvarint that decodes to 0xFFFF or 65535 in decimal. * Gameplay logic currently limits maps to 256 colors. * The default for newly created chunks in-game will be RLE by default. * Its in-memory representation is still a MapAccessor (a map of absolute world coordinates to palette index). * The game can still open and play legacy MapAccessor maps. * On save in the editor, the game will upgrade/convert MapAccessor chunks over to RLEAccessors, improving on your level's file size with a simple re-save. Current Bugs * On every re-save to RLE, one pixel is lost in the bottom-right corner of each chunk. Each subsequent re-save loses one more pixel to the left, so what starts as a single pixel per chunk slowly evolves into a horizontal line. * Some pixels smear vertically as well. * Off-by-negative-one errors when some chunks Iter() their pixels but compute a relative coordinate of (-1,0)! Some mismatch between the stored world coords of a pixel inside the chunk vs. the chunk's assigned coordinate by the Chunker: certain combinations of chunk coord/abs coord. To Do * The `doodad touch` command should re-save existing levels to upgrade them.
343 lines
7.1 KiB
Go
343 lines
7.1 KiB
Go
package level_test
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
|
"git.kirsle.net/go/render"
|
|
)
|
|
|
|
// Test the high level Chunker.
|
|
func TestChunker(t *testing.T) {
|
|
c := level.NewChunker(128)
|
|
|
|
// Test swatches.
|
|
var (
|
|
grey = &level.Swatch{
|
|
Name: "solid",
|
|
Color: render.Grey,
|
|
}
|
|
)
|
|
|
|
type testCase struct {
|
|
name string
|
|
run func() error
|
|
}
|
|
tests := []testCase{
|
|
testCase{
|
|
name: "Access a pixel on the blank map and expect an error",
|
|
run: func() error {
|
|
p := render.NewPoint(65535, -214564545)
|
|
_, err := c.Get(p)
|
|
if err == nil {
|
|
return fmt.Errorf("unexpected success getting point %s", p)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
|
|
testCase{
|
|
name: "Set a pixel",
|
|
run: func() error {
|
|
// Set a point.
|
|
p := render.NewPoint(100, 200)
|
|
err := c.Set(p, grey)
|
|
if err != nil {
|
|
return fmt.Errorf("unexpected error getting point %s: %s", p, err)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
|
|
testCase{
|
|
name: "Verify the set pixel",
|
|
run: func() error {
|
|
p := render.NewPoint(100, 200)
|
|
px, err := c.Get(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if px != grey {
|
|
return fmt.Errorf("pixel at %s not the expected color:\n"+
|
|
"Expected: %s\n"+
|
|
" Got: %s",
|
|
p,
|
|
grey,
|
|
px,
|
|
)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
|
|
testCase{
|
|
name: "Verify the neighboring pixel is unset",
|
|
run: func() error {
|
|
p := render.NewPoint(101, 200)
|
|
_, err := c.Get(p)
|
|
if err == nil {
|
|
return fmt.Errorf("unexpected success getting point %s", p)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
|
|
testCase{
|
|
name: "Delete the set pixel",
|
|
run: func() error {
|
|
p := render.NewPoint(100, 200)
|
|
err := c.Delete(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
|
|
testCase{
|
|
name: "Verify the deleted pixel is unset",
|
|
run: func() error {
|
|
p := render.NewPoint(100, 200)
|
|
_, err := c.Get(p)
|
|
if err == nil {
|
|
return fmt.Errorf("unexpected success getting point %s", p)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
|
|
testCase{
|
|
name: "Delete a pixel that didn't exist",
|
|
run: func() error {
|
|
p := render.NewPoint(-100, -100)
|
|
err := c.Delete(p)
|
|
if err == nil {
|
|
return fmt.Errorf("unexpected success deleting point %s", p)
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
if err := test.run(); err != nil {
|
|
t.Errorf("Failed: %s\n%s", test.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test the map chunk accessor.
|
|
func TestMapAccessor(t *testing.T) {
|
|
var (
|
|
c = level.NewChunk()
|
|
a = level.NewMapAccessor(c)
|
|
)
|
|
|
|
// Test action types
|
|
var (
|
|
Get = "Get"
|
|
Set = "Set"
|
|
Delete = "Delete"
|
|
)
|
|
|
|
// Test swatches.
|
|
var (
|
|
red = &level.Swatch{
|
|
Name: "fire",
|
|
Color: render.Red,
|
|
}
|
|
)
|
|
|
|
type testCase struct {
|
|
Action string
|
|
P render.Point
|
|
S *level.Swatch
|
|
Expect *level.Swatch
|
|
Err bool // expect error
|
|
}
|
|
tests := []testCase{
|
|
// Get a random point and expect to fail.
|
|
testCase{
|
|
Action: Get,
|
|
P: render.NewPoint(128, 128),
|
|
Err: true,
|
|
},
|
|
|
|
// Set a point.
|
|
testCase{
|
|
Action: Set,
|
|
S: red,
|
|
P: render.NewPoint(1024, 2048),
|
|
},
|
|
|
|
// Verify it exists.
|
|
testCase{
|
|
Action: Get,
|
|
P: render.NewPoint(1024, 2048),
|
|
Expect: red,
|
|
},
|
|
|
|
// A neighboring point does not exist.
|
|
testCase{
|
|
Action: Get,
|
|
P: render.NewPoint(1025, 2050),
|
|
Err: true,
|
|
},
|
|
|
|
// Delete a pixel that doesn't exist.
|
|
testCase{
|
|
Action: Delete,
|
|
P: render.NewPoint(1987, 2006),
|
|
Err: true,
|
|
},
|
|
|
|
// Delete one that does.
|
|
testCase{
|
|
Action: Delete,
|
|
P: render.NewPoint(1024, 2048),
|
|
},
|
|
|
|
// Verify gone
|
|
testCase{
|
|
Action: Get,
|
|
P: render.NewPoint(1024, 2048),
|
|
Err: true,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
var px *level.Swatch
|
|
var err error
|
|
switch test.Action {
|
|
case Get:
|
|
px, err = a.Get(test.P)
|
|
case Set:
|
|
err = a.Set(test.P, test.S)
|
|
case Delete:
|
|
err = a.Delete(test.P)
|
|
}
|
|
|
|
if err != nil && !test.Err {
|
|
t.Errorf("unexpected error from %s %s: %s", test.Action, test.P, err)
|
|
continue
|
|
} else if err == nil && test.Err {
|
|
t.Errorf("didn't get error when we expected from %s %s", test.Action, test.P)
|
|
continue
|
|
}
|
|
|
|
if test.Action == Get {
|
|
if px != test.Expect {
|
|
t.Errorf("didn't get expected result\n"+
|
|
"Expected: %s\n"+
|
|
" Got: %s\n",
|
|
test.Expect,
|
|
px,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test the ChunkCoordinate function.
|
|
func TestChunkCoordinates(t *testing.T) {
|
|
c := level.NewChunker(128)
|
|
|
|
type testCase struct {
|
|
WorldCoordinate render.Point
|
|
ChunkCoordinate render.Point
|
|
RelativeCoordinate render.Point
|
|
}
|
|
tests := []testCase{
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(0, 0),
|
|
ChunkCoordinate: render.NewPoint(0, 0),
|
|
RelativeCoordinate: render.NewPoint(0, 0),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(4, 8),
|
|
ChunkCoordinate: render.NewPoint(0, 0),
|
|
RelativeCoordinate: render.NewPoint(4, 8),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(128, 128),
|
|
ChunkCoordinate: render.NewPoint(1, 1),
|
|
RelativeCoordinate: render.NewPoint(0, 0),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(130, 156),
|
|
ChunkCoordinate: render.NewPoint(1, 1),
|
|
RelativeCoordinate: render.NewPoint(2, 28),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(1024, 128),
|
|
ChunkCoordinate: render.NewPoint(8, 1),
|
|
RelativeCoordinate: render.NewPoint(0, 0),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(3600, 1228),
|
|
ChunkCoordinate: render.NewPoint(28, 9),
|
|
RelativeCoordinate: render.NewPoint(16, 76),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(-100, -1),
|
|
ChunkCoordinate: render.NewPoint(-1, -1),
|
|
RelativeCoordinate: render.NewPoint(28, 127),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(-950, 100),
|
|
ChunkCoordinate: render.NewPoint(-8, 0),
|
|
RelativeCoordinate: render.NewPoint(74, 100),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(-1001, -856),
|
|
ChunkCoordinate: render.NewPoint(-8, -7),
|
|
RelativeCoordinate: render.NewPoint(23, 40),
|
|
},
|
|
testCase{
|
|
WorldCoordinate: render.NewPoint(-3600, -4800),
|
|
ChunkCoordinate: render.NewPoint(-29, -38),
|
|
RelativeCoordinate: render.NewPoint(112, 64),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
// Test conversion from world to chunk coordinate.
|
|
actual := c.ChunkCoordinate(test.WorldCoordinate)
|
|
if actual != test.ChunkCoordinate {
|
|
t.Errorf(
|
|
"Failed ChunkCoordinate conversion:\n"+
|
|
" Input: %s\n"+
|
|
"Expected: %s\n"+
|
|
" Got: %s",
|
|
test.WorldCoordinate,
|
|
test.ChunkCoordinate,
|
|
actual,
|
|
)
|
|
}
|
|
|
|
// Test the relative (inside-chunk) coordinate.
|
|
actual = level.RelativeCoordinate(test.WorldCoordinate, actual, c.Size)
|
|
if actual != test.RelativeCoordinate {
|
|
t.Errorf(
|
|
"Failed RelativeCoordinate conversion:\n"+
|
|
" Input: %s\n"+
|
|
"Expected: %s\n"+
|
|
" Got: %s",
|
|
test.WorldCoordinate,
|
|
test.RelativeCoordinate,
|
|
actual,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestZeroChunkSize(t *testing.T) {
|
|
c := &level.Chunker{}
|
|
|
|
coord := c.ChunkCoordinate(render.NewPoint(1200, 3600))
|
|
if !coord.IsZero() {
|
|
t.Errorf("ChunkCoordinate didn't fail with a zero chunk size!")
|
|
}
|
|
}
|