Save and restore maps as JSON files

First pass at a level storage format to save and restore maps.

To save a map: press F12. It takes a screenshot PNG into the
screenshots/ folder and outputs a map JSON in the working directory.

To restore a map: "go run cmd/doodle/main.go map.json"
This commit is contained in:
Noah 2018-06-17 10:29:57 -07:00
parent 407ef7f455
commit 27fafdc96d
9 changed files with 309 additions and 30 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
fonts/
screenshot-*.png
map-*.json

View File

@ -23,6 +23,15 @@ func main() {
runtime.LockOSThread()
flag.Parse()
args := flag.Args()
var filename string
if len(args) > 0 {
filename = args[0]
}
app := doodle.New(debug)
if filename != "" {
app.LoadLevel(filename)
}
app.Run()
}

View File

@ -166,6 +166,7 @@ func (d *Doodle) Loop() error {
if ev.ScreenshotKey.Pressed() {
log.Info("Taking a screenshot")
d.Screenshot()
d.SaveLevel()
}
// Clear the canvas and fill it with white.

43
draw/line.go Normal file
View File

@ -0,0 +1,43 @@
package draw
import (
"math"
"git.kirsle.net/apps/doodle/types"
)
// Line is a generator that returns the X,Y coordinates to draw a line.
// https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm)
func Line(x1, y1, x2, y2 int32) chan types.Point {
generator := make(chan types.Point)
go func() {
var (
dx = float64(x2 - x1)
dy = float64(y2 - y1)
)
var step float64
if math.Abs(dx) >= math.Abs(dy) {
step = math.Abs(dx)
} else {
step = math.Abs(dy)
}
dx = dx / step
dy = dy / step
x := float64(x1)
y := float64(y1)
for i := 0; i <= int(step); i++ {
generator <- types.Point{
X: int32(x),
Y: int32(y),
}
x += dx
y += dy
}
close(generator)
}()
return generator
}

86
draw/line_test.go Normal file
View File

@ -0,0 +1,86 @@
package draw_test
import (
"fmt"
"testing"
"git.kirsle.net/apps/doodle/draw"
"git.kirsle.net/apps/doodle/types"
)
func TestLine(t *testing.T) {
type task struct {
X1 int32
X2 int32
Y1 int32
Y2 int32
Expect []types.Point
}
toString := func(t task) string {
return fmt.Sprintf("Line<%d,%d -> %d,%d>",
t.X1, t.Y1,
t.X2, t.Y2,
)
}
var tasks = []task{
task{
X1: 0,
Y1: 0,
X2: 0,
Y2: 10,
Expect: []types.Point{
{X: 0, Y: 0},
{X: 0, Y: 1},
{X: 0, Y: 2},
{X: 0, Y: 3},
{X: 0, Y: 4},
{X: 0, Y: 5},
{X: 0, Y: 6},
{X: 0, Y: 7},
{X: 0, Y: 8},
{X: 0, Y: 9},
{X: 0, Y: 10},
},
},
task{
X1: 10,
Y1: 10,
X2: 15,
Y2: 15,
Expect: []types.Point{
{X: 10, Y: 10},
{X: 11, Y: 11},
{X: 12, Y: 12},
{X: 13, Y: 13},
{X: 14, Y: 14},
{X: 15, Y: 15},
},
},
}
for _, test := range tasks {
gen := draw.Line(test.X1, test.Y1, test.X2, test.Y2)
var i int
for point := range gen {
if i >= len(test.Expect) {
t.Errorf("%s: Got more pixels back than expected: %s",
toString(test),
point,
)
break
}
expect := test.Expect[i]
if expect != point {
t.Errorf("%s: at index %d I got %s but expected %s",
toString(test),
i,
point,
expect,
)
}
i++
}
}
}

30
level/json.go Normal file
View File

@ -0,0 +1,30 @@
package level
import (
"bytes"
"encoding/json"
"os"
)
// ToJSON serializes the level as JSON.
func (m *Level) ToJSON() ([]byte, error) {
out := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(out)
encoder.SetIndent("", "\t")
err := encoder.Encode(m)
return out.Bytes(), err
}
// LoadJSON loads a map from JSON file.
func LoadJSON(filename string) (Level, error) {
fh, err := os.Open(filename)
if err != nil {
return Level{}, err
}
defer fh.Close()
m := Level{}
decoder := json.NewDecoder(fh)
err = decoder.Decode(&m)
return m, err
}

40
level/types.go Normal file
View File

@ -0,0 +1,40 @@
package level
// Level is the container format for Doodle map drawings.
type Level struct {
Version int32 `json:"version"` // File format version spec.
Title string `json:"title"`
Author string `json:"author"`
Password string `json:"passwd"`
Locked bool `json:"locked"`
// Level size.
Width int32 `json:"w"`
Height int32 `json:"h"`
// The Palette holds the unique "colors" used in this map file, and their
// properties (solid, fire, slippery, etc.)
Palette []Palette `json:"palette"`
// Pixels is a 2D array indexed by [X][Y]. The cell values are indexes into
// the Palette.
Pixels []Pixel `json:"pixels"`
}
// Pixel associates a coordinate with a palette index.
type Pixel struct {
X int32 `json:"x"`
Y int32 `json:"y"`
Palette int32 `json:"p"`
}
// Palette are the unique pixel attributes that this map uses, and serves
// as a lookup table for the Pixels.
type Palette struct {
// Required attributes.
Color string `json:"color"`
// Optional attributes.
Solid bool `json:"solid,omitempty"`
Fire bool `json:"fire,omitempty"`
}

View File

@ -4,11 +4,83 @@ import (
"fmt"
"image"
"image/png"
"math"
"io/ioutil"
"os"
"time"
"git.kirsle.net/apps/doodle/draw"
"git.kirsle.net/apps/doodle/level"
)
// SaveLevel saves the level to disk.
func (d *Doodle) SaveLevel() {
m := level.Level{
Version: 1,
Title: "Alpha",
Author: os.Getenv("USER"),
Width: d.width,
Height: d.height,
Palette: []level.Palette{
level.Palette{
Color: "#000000",
Solid: true,
},
},
Pixels: []level.Pixel{},
}
for pixel := range d.canvas {
for point := range draw.Line(pixel.x, pixel.y, pixel.dx, pixel.dy) {
m.Pixels = append(m.Pixels, level.Pixel{
X: point.X,
Y: point.Y,
Palette: 0,
})
}
}
json, err := m.ToJSON()
if err != nil {
log.Error("SaveLevel error: %s", err)
return
}
filename := fmt.Sprintf("./map-%s.json",
time.Now().Format("2006-01-02T15-04-05"),
)
err = ioutil.WriteFile(filename, json, 0644)
if err != nil {
log.Error("Create map file error: %s", err)
return
}
}
// LoadLevel loads a map from JSON.
func (d *Doodle) LoadLevel(filename string) error {
log.Info("Loading level from file: %s", filename)
pixelHistory = []Pixel{}
d.canvas = Grid{}
m, err := level.LoadJSON(filename)
if err != nil {
return err
}
for _, point := range m.Pixels {
pixel := Pixel{
start: true,
x: point.X,
y: point.Y,
dx: point.X,
dy: point.Y,
}
pixelHistory = append(pixelHistory, pixel)
d.canvas[pixel] = nil
}
return nil
}
// Screenshot saves the level canvas to disk as a PNG image.
func (d *Doodle) Screenshot() {
screenshot := image.NewRGBA(image.Rect(0, 0, int(d.width), int(d.height)))
@ -26,39 +98,23 @@ func (d *Doodle) Screenshot() {
if pixel.x == pixel.dx && pixel.y == pixel.dy {
screenshot.Set(int(pixel.x), int(pixel.y), image.Black)
} else {
// Draw a line. TODO: get this into its own function!
// https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm)
var (
x1 = pixel.x
x2 = pixel.dx
y1 = pixel.y
y2 = pixel.dy
)
var (
dx = float64(x2 - x1)
dy = float64(y2 - y1)
)
var step float64
if math.Abs(dx) >= math.Abs(dy) {
step = math.Abs(dx)
} else {
step = math.Abs(dy)
for point := range draw.Line(pixel.x, pixel.y, pixel.dx, pixel.dy) {
screenshot.Set(int(point.X), int(point.Y), image.Black)
}
dx = dx / step
dy = dy / step
x := float64(x1)
y := float64(y1)
for i := 0; i <= int(step); i++ {
screenshot.Set(int(x), int(y), image.Black)
x += dx
y += dy
}
}
// Create the screenshot directory.
if _, err := os.Stat("./screenshots"); os.IsNotExist(err) {
log.Info("Creating directory: ./screenshots")
err = os.Mkdir("./screenshots", 0755)
if err != nil {
log.Error("Can't create ./screenshots: %s", err)
return
}
}
filename := fmt.Sprintf("screenshot-%s.png",
filename := fmt.Sprintf("./screenshots/screenshot-%s.png",
time.Now().Format("2006-01-02T15-04-05"),
)
fh, err := os.Create(filename)

13
types/types.go Normal file
View File

@ -0,0 +1,13 @@
package types
import "fmt"
// Point is a 2D point in space.
type Point struct {
X int32 `json:"x"`
Y int32 `json:"y"`
}
func (p Point) String() string {
return fmt.Sprintf("(%d,%d)", p.X, p.Y)
}