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/
|
||||
screenshot-*.png
|
||||
map-*.json
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
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"`
|
||||
}
|
116
screenshot.go
116
screenshot.go
|
@ -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)
|
||||
}
|
||||
|
||||
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
|
||||
for point := range draw.Line(pixel.x, pixel.y, pixel.dx, pixel.dy) {
|
||||
screenshot.Set(int(point.X), int(point.Y), image.Black)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("screenshot-%s.png",
|
||||
// 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("./screenshots/screenshot-%s.png",
|
||||
time.Now().Format("2006-01-02T15-04-05"),
|
||||
)
|
||||
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