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:
parent
407ef7f455
commit
27fafdc96d
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
fonts/
|
fonts/
|
||||||
screenshot-*.png
|
screenshot-*.png
|
||||||
|
map-*.json
|
||||||
|
|
|
@ -23,6 +23,15 @@ func main() {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
var filename string
|
||||||
|
if len(args) > 0 {
|
||||||
|
filename = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
app := doodle.New(debug)
|
app := doodle.New(debug)
|
||||||
|
if filename != "" {
|
||||||
|
app.LoadLevel(filename)
|
||||||
|
}
|
||||||
app.Run()
|
app.Run()
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,6 +166,7 @@ func (d *Doodle) Loop() error {
|
||||||
if ev.ScreenshotKey.Pressed() {
|
if ev.ScreenshotKey.Pressed() {
|
||||||
log.Info("Taking a screenshot")
|
log.Info("Taking a screenshot")
|
||||||
d.Screenshot()
|
d.Screenshot()
|
||||||
|
d.SaveLevel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the canvas and fill it with white.
|
// Clear the canvas and fill it with white.
|
||||||
|
|
43
draw/line.go
Normal file
43
draw/line.go
Normal 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
86
draw/line_test.go
Normal 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
30
level/json.go
Normal 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
40
level/types.go
Normal 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"`
|
||||||
|
}
|
112
screenshot.go
112
screenshot.go
|
@ -4,11 +4,83 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"math"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"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.
|
// Screenshot saves the level canvas to disk as a PNG image.
|
||||||
func (d *Doodle) Screenshot() {
|
func (d *Doodle) Screenshot() {
|
||||||
screenshot := image.NewRGBA(image.Rect(0, 0, int(d.width), int(d.height)))
|
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 {
|
if pixel.x == pixel.dx && pixel.y == pixel.dy {
|
||||||
screenshot.Set(int(pixel.x), int(pixel.y), image.Black)
|
screenshot.Set(int(pixel.x), int(pixel.y), image.Black)
|
||||||
} else {
|
} else {
|
||||||
// Draw a line. TODO: get this into its own function!
|
for point := range draw.Line(pixel.x, pixel.y, pixel.dx, pixel.dy) {
|
||||||
// https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm)
|
screenshot.Set(int(point.X), int(point.Y), image.Black)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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"),
|
time.Now().Format("2006-01-02T15-04-05"),
|
||||||
)
|
)
|
||||||
fh, err := os.Create(filename)
|
fh, err := os.Create(filename)
|
||||||
|
|
13
types/types.go
Normal file
13
types/types.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user