From 27fafdc96d33492cd742a3020fa9fef6f5b1ef07 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 17 Jun 2018 10:29:57 -0700 Subject: [PATCH] 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" --- .gitignore | 1 + cmd/doodle/main.go | 9 ++++ doodle.go | 1 + draw/line.go | 43 +++++++++++++++++ draw/line_test.go | 86 +++++++++++++++++++++++++++++++++ level/json.go | 30 ++++++++++++ level/types.go | 40 ++++++++++++++++ screenshot.go | 116 +++++++++++++++++++++++++++++++++------------ types/types.go | 13 +++++ 9 files changed, 309 insertions(+), 30 deletions(-) create mode 100644 draw/line.go create mode 100644 draw/line_test.go create mode 100644 level/json.go create mode 100644 level/types.go create mode 100644 types/types.go diff --git a/.gitignore b/.gitignore index 09b771b..8b63e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ fonts/ screenshot-*.png +map-*.json diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 56e921d..688cb39 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -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() } diff --git a/doodle.go b/doodle.go index 74241d8..36f8228 100644 --- a/doodle.go +++ b/doodle.go @@ -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. diff --git a/draw/line.go b/draw/line.go new file mode 100644 index 0000000..45dfab5 --- /dev/null +++ b/draw/line.go @@ -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 +} diff --git a/draw/line_test.go b/draw/line_test.go new file mode 100644 index 0000000..d796ae3 --- /dev/null +++ b/draw/line_test.go @@ -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++ + } + } +} diff --git a/level/json.go b/level/json.go new file mode 100644 index 0000000..fbb83ca --- /dev/null +++ b/level/json.go @@ -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 +} diff --git a/level/types.go b/level/types.go new file mode 100644 index 0000000..0a4fd9e --- /dev/null +++ b/level/types.go @@ -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"` +} diff --git a/screenshot.go b/screenshot.go index 67cfb96..67b2905 100644 --- a/screenshot.go +++ b/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) diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000..14a2644 --- /dev/null +++ b/types/types.go @@ -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) +}