From a4058df9c881e531002a40a933e100cfc53cb5e3 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Wed, 15 Jan 2020 19:03:52 +0100 Subject: [PATCH] Moved level to separate package (soko). Moved lineparser to gut package. --- cmd/krampus19/entity.go | 5 +- cmd/krampus19/level.go | 142 -------------------------------- cmd/krampus19/levelpack.go | 52 ++++++------ cmd/krampus19/lineparser.go | 64 -------------- cmd/krampus19/playlevel.go | 23 +++--- cmd/krampus19/playlevelstate.go | 49 +++++------ cmd/krampus19/sprite.go | 45 +++++----- gut/lineparser.go | 66 +++++++++++++++ soko/level.go | 62 ++++++++++++++ soko/levelparser.go | 85 +++++++++++++++++++ 10 files changed, 305 insertions(+), 288 deletions(-) delete mode 100644 cmd/krampus19/level.go delete mode 100644 cmd/krampus19/lineparser.go create mode 100644 gut/lineparser.go create mode 100644 soko/level.go create mode 100644 soko/levelparser.go diff --git a/cmd/krampus19/entity.go b/cmd/krampus19/entity.go index 68c478b..b2fc03a 100644 --- a/cmd/krampus19/entity.go +++ b/cmd/krampus19/entity.go @@ -2,10 +2,11 @@ package main import ( "opslag.de/schobers/geom" + "opslag.de/schobers/krampus19/soko" ) type entity struct { - typ entityType + typ soko.EntityType pos geom.Point scr entityLoc } @@ -15,6 +16,6 @@ type entityLoc struct { z float32 } -func newEntity(typ entityType, pos geom.Point) *entity { +func newEntity(typ soko.EntityType, pos geom.Point) *entity { return &entity{typ, pos, entityLoc{pos.ToF32(), 0}} } diff --git a/cmd/krampus19/level.go b/cmd/krampus19/level.go deleted file mode 100644 index efc37e3..0000000 --- a/cmd/krampus19/level.go +++ /dev/null @@ -1,142 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "io" - - "opslag.de/schobers/geom" -) - -type entityType byte -type tile byte - -const ( - entityTypeInvalid entityType = entityType(0) - entityTypeNone = '_' - entityTypeCharacter = '@' - entityTypeEgg = 'X' - entityTypeBrick = 'B' -) - -func (e entityType) IsValid() bool { - switch e { - case entityTypeNone: - case entityTypeCharacter: - case entityTypeEgg: - case entityTypeBrick: - default: - return false - } - return true -} - -const ( - tileInvalid tile = tile(0) - tileNothing = '.' - tileBasic = '#' - tileMagma = '~' -) - -func (t tile) IsValid() bool { - switch t { - case tileNothing: - case tileBasic: - case tileMagma: - default: - return false - } - return true -} - -type level struct { - width int - height int - tiles []tile - entities []entityType -} - -func (l level) idxToPos(i int) geom.Point { return geom.Pt(i%l.width, i/l.width) } - -func (l level) posToIdx(p geom.Point) int { - if p.X < 0 || p.Y < 0 || p.X >= l.width || p.Y >= l.height { - return -1 - } - return p.Y*l.width + p.X -} - -func parseLevelAsset(r io.Reader) (level, error) { - var l level - ctx := levelContext{&l} - err := parseLines(r, ctx.parse) - if err != nil { - return level{}, err - } - return l, nil -} - -type levelContext struct { - level *level -} - -func (c *levelContext) parse(p *lineParser) parseLineFn { - if p.eof() { - return nil - } - switch p.peek() { - case "level:": - return c.parseContent - case "": - p.next() // skip - return c.parse - default: - return nil - } -} - -func (c *levelContext) parseContent(p *lineParser) parseLineFn { - if p.next() != "level:" { - return p.emitErr(errors.New("expected level start")) - } - return c.parseRow -} - -func (c *levelContext) parseRow(p *lineParser) parseLineFn { - if p.eof() { - return p.emitErr(errors.New("unexpected end of file")) - } - line := p.next() - if line == ":level" { - return c.parse - } - if c.level.height == 0 { - c.level.width = len(line) / 2 - } - return c.addRow(p, line) -} - -func (c *levelContext) addRow(p *lineParser, line string) parseLineFn { - var tiles []tile - var entities []entityType - for i := 0; i < len(line); i += 2 { - tiles = append(tiles, tile(line[i])) - entities = append(entities, entityType(line[i+1])) - } - - for i, t := range tiles { - if !t.IsValid() { - return p.emitErr(fmt.Errorf("level contains invalid tile at (%d, %d)", i, c.level.height)) - } - } - for i, e := range entities { - if !e.IsValid() { - return p.emitErr(fmt.Errorf("level contains invalid entity type at (%d, %d)", i, c.level.height)) - } - } - - c.level.height++ - c.level.tiles = append(c.level.tiles, tiles...) - c.level.entities = append(c.level.entities, entities...) - - return c.parseRow -} diff --git a/cmd/krampus19/levelpack.go b/cmd/krampus19/levelpack.go index 7116a62..33bc333 100644 --- a/cmd/krampus19/levelpack.go +++ b/cmd/krampus19/levelpack.go @@ -4,12 +4,16 @@ import ( "errors" "io" "strings" + + "opslag.de/schobers/krampus19/gut" + + "opslag.de/schobers/krampus19/soko" ) type levelPack struct { name string order []string - levels map[string]level + levels map[string]soko.Level } func (p levelPack) find(level string) int { @@ -38,19 +42,19 @@ type parseLevelPackContext struct { func parseLevelPackAsset(r io.Reader, openLevelFn func(id string) (io.ReadCloser, error)) (levelPack, error) { ctx := &parseLevelPackContext{} - err := parseLines(r, ctx.Parse) + err := gut.ParseLines(r, ctx.Parse) if err != nil { return levelPack{}, err } - pack := levelPack{name: ctx.name, levels: map[string]level{}} + pack := levelPack{name: ctx.name, levels: map[string]soko.Level{}} for _, id := range ctx.levels { rc, err := openLevelFn(id) if err != nil { return levelPack{}, err } defer rc.Close() - level, err := parseLevelAsset(rc) + level, err := soko.ParseLevel(rc) if err != nil { return levelPack{}, err } @@ -61,65 +65,65 @@ func parseLevelPackAsset(r io.Reader, openLevelFn func(id string) (io.ReadCloser return pack, nil } -func (c *parseLevelPackContext) Parse(p *lineParser) parseLineFn { - if p.skipSpaceEOF() { - return p.emitErr(errors.New("empty level pack")) +func (c *parseLevelPackContext) Parse(p *gut.LineParser) gut.ParseLineFn { + if p.SkipSpaceEOF() { + return p.EmitErr(errors.New("empty level pack")) } return c.parse } -func (c *parseLevelPackContext) parse(p *lineParser) parseLineFn { +func (c *parseLevelPackContext) parse(p *gut.LineParser) gut.ParseLineFn { const levelsTag = "levels:" const nameTag = "name:" - if p.skipSpaceEOF() { + if p.SkipSpaceEOF() { return nil } - line := p.next() + line := p.Next() switch { case strings.HasPrefix(line, nameTag): c.name = strings.TrimSpace(line[len(nameTag):]) return c.parse case strings.HasPrefix(line, levelsTag): - return skipSpaceBeforeContent(c.parseLevels) + return gut.SkipSpaceBeforeContent(c.parseLevels) } - return p.emitErr(errors.New("tag not allowed")) + return p.EmitErr(errors.New("tag not allowed")) } -func (c *parseLevelPackContext) parseLevels(p *lineParser) parseLineFn { +func (c *parseLevelPackContext) parseLevels(p *gut.LineParser) gut.ParseLineFn { const levelTag = "level:" const levelsEndTag = ":levels" - line := p.next() + line := p.Next() switch line { case levelTag: - return skipSpaceBeforeContent(c.parseLevel) + return gut.SkipSpaceBeforeContent(c.parseLevel) case levelsEndTag: return c.parse } - return p.emitErr(errors.New("tag not allowed")) + return p.EmitErr(errors.New("tag not allowed")) } -func (c *parseLevelPackContext) parseLevel(p *lineParser) parseLineFn { +func (c *parseLevelPackContext) parseLevel(p *gut.LineParser) gut.ParseLineFn { const idTag = "id:" - line := p.next() + line := p.Next() switch { case strings.HasPrefix(line, idTag): c.levels = append(c.levels, strings.TrimSpace(line[len(idTag):])) - return skipSpaceBeforeContent(c.parseLevelEnd) + return gut.SkipSpaceBeforeContent(c.parseLevelEnd) } - return p.emitErr(errors.New("must have an id tag")) + return p.EmitErr(errors.New("must have an id tag")) } -func (c *parseLevelPackContext) parseLevelEnd(p *lineParser) parseLineFn { +func (c *parseLevelPackContext) parseLevelEnd(p *gut.LineParser) gut.ParseLineFn { const levelEndTag = ":level" - line := p.next() + line := p.Next() switch { case strings.HasPrefix(line, levelEndTag): - return skipSpaceBeforeContent(c.parseLevels) + return gut.SkipSpaceBeforeContent(c.parseLevels) } - return p.emitErr(errors.New("tag not allowed")) + return p.EmitErr(errors.New("tag not allowed")) } diff --git a/cmd/krampus19/lineparser.go b/cmd/krampus19/lineparser.go deleted file mode 100644 index da0db98..0000000 --- a/cmd/krampus19/lineparser.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "errors" - "io" - "io/ioutil" - "strings" -) - -type lineParser struct { - lines []string - i int - err error -} - -var errUnexpectedEnd = errors.New("unexpected end of file") - -func (p *lineParser) eof() bool { return p.i == len(p.lines) } -func (p *lineParser) peek() string { return p.lines[p.i] } -func (p *lineParser) next() string { - i := p.i - p.i++ - return p.lines[i] -} - -func (p *lineParser) emitErr(err error) parseLineFn { - p.err = err - return nil -} - -func (p *lineParser) skipSpaceEOF() bool { - for !p.eof() && len(strings.TrimSpace(p.peek())) == 0 { - p.next() - } - return p.eof() -} - -func skipSpaceBeforeContent(next parseLineFn) parseLineFn { - return func(p *lineParser) parseLineFn { - if p.skipSpaceEOF() { - return p.emitErr(errUnexpectedEnd) - } - return next - } -} - -type parseLineFn func(p *lineParser) parseLineFn - -func parseLines(r io.Reader, fn parseLineFn) error { - content, err := ioutil.ReadAll(r) - if err != nil { - return err - } - lines := strings.Split(string(content), "\n") - for i, line := range lines { - lines[i] = strings.TrimRight(line, "\r\n") - } - - parser := &lineParser{lines: lines} - for fn != nil { - fn = fn(parser) - } - return parser.err -} diff --git a/cmd/krampus19/playlevel.go b/cmd/krampus19/playlevel.go index 311e529..5b183bc 100644 --- a/cmd/krampus19/playlevel.go +++ b/cmd/krampus19/playlevel.go @@ -8,6 +8,7 @@ import ( "opslag.de/schobers/allg5" "opslag.de/schobers/geom" "opslag.de/schobers/krampus19/alui" + "opslag.de/schobers/krampus19/soko" ) type playLevel struct { @@ -121,13 +122,13 @@ func (l *playLevel) Layout(ctx *alui.Context, bounds geom.RectangleF32) { l.offset = geom.PointF32{} level := l.state.Level() - var contentCenter = l.posToScreenF32(geom.PtF32(.5*float32(level.width), .5*float32(level.height)), 0) + var contentCenter = l.posToScreenF32(geom.PtF32(.5*float32(level.Width), .5*float32(level.Height)), 0) var content = geom.RectF32(contentCenter.X, contentCenter.Y, contentCenter.X, contentCenter.Y) - for idx, tile := range l.state.Level().tiles { - if tile == tileNothing || tile == tileInvalid { + for idx, tile := range l.state.Level().Tiles { + if tile == soko.TileNothing || tile == soko.TileInvalid { continue } - pos := level.idxToPos(idx).ToF32() + pos := level.IdxToPos(idx).ToF32() bottomLeft := l.posToScreenF32(pos.Add2D(-1.5, 1.5), 100) content.Min = geom.MinPtF32(content.Min, bottomLeft) content.Max = geom.MaxPtF32(content.Max, bottomLeft) @@ -191,17 +192,17 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { } } - for i, t := range level.tiles { - pos := geom.Pt(i%level.width, i/level.width) + for i, t := range level.Tiles { + pos := geom.Pt(i%level.Width, i/level.Width) scr := entityLoc{pos.ToF32(), 0} switch t { - case tileBasic: + case soko.TileBasic: if l.state.IsNextToMagma(pos) { l.drawSprite("lava_brick", "magma", scr) } else { l.drawSprite("lava_brick", "lava_brick", scr) } - case tileMagma: + case soko.TileMagma: l.drawSprite("magma", "magma", scr) brick := l.state.FindSunkenBrick(pos) if brick != nil { @@ -224,11 +225,11 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { for _, e := range entities { switch e.typ { - case entityTypeBrick: + case soko.EntityTypeBrick: l.drawSprite("brick", "brick", e.scr) - case entityTypeCharacter: + case soko.EntityTypeCharacter: l.drawSprite("dragon", "dragon", e.scr) - case entityTypeEgg: + case soko.EntityTypeEgg: l.drawSprite("egg", "egg", e.scr) } } diff --git a/cmd/krampus19/playlevelstate.go b/cmd/krampus19/playlevelstate.go index 92f1588..722e367 100644 --- a/cmd/krampus19/playlevelstate.go +++ b/cmd/krampus19/playlevelstate.go @@ -7,13 +7,14 @@ import ( "opslag.de/schobers/allg5" "opslag.de/schobers/geom" "opslag.de/schobers/krampus19/gut" + "opslag.de/schobers/krampus19/soko" ) type playLevelState struct { ctx *Context pack levelPack - level level + level soko.Level player *entity egg *entity bricks entityList @@ -66,21 +67,21 @@ func (s *playLevelState) Init(ctx *Context, pack, level string, onComplete func( s.bricks = nil s.sunken = nil s.splash = map[geom.Point]*splashAnimation{} - for i, e := range s.level.entities { + for i, e := range s.level.Entities { switch e { - case entityTypeBrick: - s.bricks = append(s.bricks, newEntity(e, s.level.idxToPos(i))) - case entityTypeCharacter: - s.player = newEntity(e, s.level.idxToPos(i)) - case entityTypeEgg: - s.egg = newEntity(e, s.level.idxToPos(i)) + case soko.EntityTypeBrick: + s.bricks = append(s.bricks, newEntity(e, s.level.IdxToPos(i))) + case soko.EntityTypeCharacter: + s.player = newEntity(e, s.level.IdxToPos(i)) + case soko.EntityTypeEgg: + s.egg = newEntity(e, s.level.IdxToPos(i)) } } s.keysDown = keyPressedState{} s.onComplete = onComplete } -func (s *playLevelState) Level() level { return s.level } +func (s *playLevelState) Level() soko.Level { return s.level } func (s *playLevelState) PressKey(key allg5.Key) { s.keysDown[key] = true @@ -176,16 +177,16 @@ func (s *playLevelState) canMove(from, dir geom.Point) bool { return true } -func (s *playLevelState) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool) bool { +func (s *playLevelState) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t soko.Tile) bool) bool { return s.checkTileNotFound(pos, check, false) } -func (s *playLevelState) checkTileNotFound(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool, notFound bool) bool { - idx := s.level.posToIdx(pos) +func (s *playLevelState) checkTileNotFound(pos geom.Point, check func(pos geom.Point, idx int, t soko.Tile) bool, notFound bool) bool { + idx := s.level.PosToIdx(pos) if idx == -1 { return notFound } - return check(pos, idx, s.level.tiles[idx]) + return check(pos, idx, s.level.Tiles[idx]) } func (s *playLevelState) findEntityAt(pos geom.Point) *entity { @@ -199,31 +200,33 @@ func (s *playLevelState) findEntityAt(pos geom.Point) *entity { return s.sunken.FindEntity(pos) } -func (s *playLevelState) isObstructed(pos geom.Point, idx int, t tile) bool { +func (s *playLevelState) isObstructed(pos geom.Point, idx int, t soko.Tile) bool { if s.bricks.FindEntity(pos) != nil { return true // brick } - switch s.level.tiles[idx] { - case tileMagma: + switch s.level.Tiles[idx] { + case soko.TileMagma: return false - case tileBasic: + case soko.TileBasic: return false } return true } -func (s *playLevelState) isMagma(pos geom.Point, idx int, t tile) bool { return t == tileMagma } +func (s *playLevelState) isMagma(pos geom.Point, idx int, t soko.Tile) bool { + return t == soko.TileMagma +} -func (s *playLevelState) isSolidTile(pos geom.Point, idx int, t tile) bool { +func (s *playLevelState) isSolidTile(pos geom.Point, idx int, t soko.Tile) bool { switch t { - case tileBasic: + case soko.TileBasic: return true - case tileMagma: + case soko.TileMagma: return s.sunken.FindEntity(pos) != nil } return false } -func (s *playLevelState) wouldBrickSink(pos geom.Point, idx int, t tile) bool { - return t == tileMagma && s.sunken.FindEntity(pos) == nil +func (s *playLevelState) wouldBrickSink(pos geom.Point, idx int, t soko.Tile) bool { + return t == soko.TileMagma && s.sunken.FindEntity(pos) == nil } diff --git a/cmd/krampus19/sprite.go b/cmd/krampus19/sprite.go index d14701c..cb17b3c 100644 --- a/cmd/krampus19/sprite.go +++ b/cmd/krampus19/sprite.go @@ -7,6 +7,7 @@ import ( "strings" "opslag.de/schobers/geom" + "opslag.de/schobers/krampus19/gut" ) type sprite struct { @@ -33,7 +34,7 @@ type spritePart struct { func loadSpriteAsset(r io.Reader) (sprite, error) { var l sprite ctx := spriteContext{&l, nil} - err := parseLines(r, ctx.parse) + err := gut.ParseLines(r, ctx.parse) if err != nil { return sprite{}, err } @@ -45,44 +46,44 @@ type spriteContext struct { part *spritePart } -func (c *spriteContext) parse(p *lineParser) parseLineFn { - if p.skipSpaceEOF() { +func (c *spriteContext) parse(p *gut.LineParser) gut.ParseLineFn { + if p.SkipSpaceEOF() { return nil } - line := p.peek() + line := p.Peek() switch { case strings.HasPrefix(line, "sprite:"): - p.next() + p.Next() return c.parseContent default: return nil } } -func (c *spriteContext) parseContent(p *lineParser) parseLineFn { +func (c *spriteContext) parseContent(p *gut.LineParser) gut.ParseLineFn { const partTag = "part:" const textureTag = "texture:" const spriteEndTag = ":sprite" - if p.skipSpaceEOF() { - return p.emitErr(errUnexpectedEnd) + if p.SkipSpaceEOF() { + return p.EmitErr(gut.ErrUnexpectedEnd) } - line := p.peek() + line := p.Peek() switch { case strings.HasPrefix(line, textureTag): c.sprite.texture = strings.TrimSpace(line[len(textureTag):]) - p.next() + p.Next() return c.parseContent case line == partTag: - p.next() + p.Next() c.part = &spritePart{} return c.parsePart case line == spriteEndTag: return nil } - return p.emitErr(errors.New("unexpected content of sprite")) + return p.EmitErr(errors.New("unexpected content of sprite")) } -func (c *spriteContext) parsePart(p *lineParser) parseLineFn { +func (c *spriteContext) parsePart(p *gut.LineParser) gut.ParseLineFn { mustAtois := func(s ...string) []int { res := make([]int, len(s)) var err error @@ -110,14 +111,14 @@ func (c *spriteContext) parsePart(p *lineParser) parseLineFn { const anchorTag = "anchor:" const scaleTag = "scale:" const partEndTag = ":part" - if p.skipSpaceEOF() { - return p.emitErr(errUnexpectedEnd) + if p.SkipSpaceEOF() { + return p.EmitErr(gut.ErrUnexpectedEnd) } - line := p.peek() + line := p.Peek() switch { case strings.HasPrefix(line, nameTag): c.part.name = strings.TrimSpace(line[len(nameTag):]) - p.next() + p.Next() return c.parsePart case strings.HasPrefix(line, subTextureTag): var coords = mustCoords(line[len(subTextureTag):]) @@ -125,7 +126,7 @@ func (c *spriteContext) parsePart(p *lineParser) parseLineFn { panic("expected four coordinates (min x, min y, size x, size y)") } c.part.sub = geom.Rect(coords[0], coords[1], coords[0]+coords[2], coords[1]+coords[3]) - p.next() + p.Next() return c.parsePart case strings.HasPrefix(line, anchorTag): var coords = mustCoords(line[len(anchorTag):]) @@ -133,16 +134,16 @@ func (c *spriteContext) parsePart(p *lineParser) parseLineFn { panic("expected two coordinates (min x, min y)") } c.part.anchor = geom.Pt(coords[0], coords[1]) - p.next() + p.Next() return c.parsePart case strings.HasPrefix(line, scaleTag): c.part.scale = mustAtof(line[len(scaleTag):]) - p.next() + p.Next() return c.parsePart case line == partEndTag: c.sprite.parts = append(c.sprite.parts, *c.part) - p.next() + p.Next() return c.parseContent } - return p.emitErr(errors.New("unexpected content of part")) + return p.EmitErr(errors.New("unexpected content of part")) } diff --git a/gut/lineparser.go b/gut/lineparser.go new file mode 100644 index 0000000..0d1fa06 --- /dev/null +++ b/gut/lineparser.go @@ -0,0 +1,66 @@ +package gut + +import ( + "errors" + "io" + "io/ioutil" + "strings" +) + +type LineParser struct { + lines []string + i int + err error +} + +var ErrUnexpectedEnd = errors.New("unexpected end of file") + +func (p *LineParser) EOF() bool { return p.i == len(p.lines) } + +func (p *LineParser) EmitErr(err error) ParseLineFn { + p.err = err + return nil +} + +func (p *LineParser) Next() string { + i := p.i + p.i++ + return p.lines[i] +} + +func (p *LineParser) Peek() string { return p.lines[p.i] } + +func (p *LineParser) SkipSpaceEOF() bool { + for !p.EOF() && len(strings.TrimSpace(p.Peek())) == 0 { + p.Next() + } + return p.EOF() +} + +func SkipSpaceBeforeContent(next ParseLineFn) ParseLineFn { + return func(p *LineParser) ParseLineFn { + if p.SkipSpaceEOF() { + return p.EmitErr(ErrUnexpectedEnd) + } + return next + } +} + +type ParseLineFn func(p *LineParser) ParseLineFn + +func ParseLines(r io.Reader, fn ParseLineFn) error { + content, err := ioutil.ReadAll(r) + if err != nil { + return err + } + lines := strings.Split(string(content), "\n") + for i, line := range lines { + lines[i] = strings.TrimRight(line, "\r\n") + } + + parser := &LineParser{lines: lines} + for fn != nil { + fn = fn(parser) + } + return parser.err +} diff --git a/soko/level.go b/soko/level.go new file mode 100644 index 0000000..beaed7a --- /dev/null +++ b/soko/level.go @@ -0,0 +1,62 @@ +package soko + +import ( + "opslag.de/schobers/geom" +) + +type EntityType byte +type Tile byte + +const ( + EntityTypeInvalid EntityType = EntityType(0) + EntityTypeNone = '_' + EntityTypeCharacter = '@' + EntityTypeEgg = 'X' + EntityTypeBrick = 'B' +) + +func (e EntityType) IsValid() bool { + switch e { + case EntityTypeNone: + case EntityTypeCharacter: + case EntityTypeEgg: + case EntityTypeBrick: + default: + return false + } + return true +} + +const ( + TileInvalid Tile = Tile(0) + TileNothing = '.' + TileBasic = '#' + TileMagma = '~' +) + +func (t Tile) IsValid() bool { + switch t { + case TileNothing: + case TileBasic: + case TileMagma: + default: + return false + } + return true +} + +type Level struct { + Width int + Height int + Tiles []Tile + Entities []EntityType +} + +func (l Level) IdxToPos(i int) geom.Point { return geom.Pt(i%l.Width, i/l.Width) } + +func (l Level) PosToIdx(p geom.Point) int { + if p.X < 0 || p.Y < 0 || p.X >= l.Width || p.Y >= l.Height { + return -1 + } + return p.Y*l.Width + p.X +} diff --git a/soko/levelparser.go b/soko/levelparser.go new file mode 100644 index 0000000..03ba97f --- /dev/null +++ b/soko/levelparser.go @@ -0,0 +1,85 @@ +package soko + +import ( + "errors" + "fmt" + "io" + + "opslag.de/schobers/krampus19/gut" +) + +func ParseLevel(r io.Reader) (Level, error) { + var l Level + ctx := levelContext{&l} + err := gut.ParseLines(r, ctx.parse) + if err != nil { + return Level{}, err + } + return l, nil +} + +type levelContext struct { + level *Level +} + +func (c *levelContext) parse(p *gut.LineParser) gut.ParseLineFn { + if p.EOF() { + return nil + } + switch p.Peek() { + case "level:": + return c.parseContent + case "": + p.Next() // skip + return c.parse + default: + return nil + } +} + +func (c *levelContext) parseContent(p *gut.LineParser) gut.ParseLineFn { + if p.Next() != "level:" { + return p.EmitErr(errors.New("expected level start")) + } + return c.parseRow +} + +func (c *levelContext) parseRow(p *gut.LineParser) gut.ParseLineFn { + if p.EOF() { + return p.EmitErr(errors.New("unexpected end of file")) + } + line := p.Next() + if line == ":level" { + return c.parse + } + if c.level.Height == 0 { + c.level.Width = len(line) / 2 + } + return c.addRow(p, line) +} + +func (c *levelContext) addRow(p *gut.LineParser, line string) gut.ParseLineFn { + var tiles []Tile + var entities []EntityType + for i := 0; i < len(line); i += 2 { + tiles = append(tiles, Tile(line[i])) + entities = append(entities, EntityType(line[i+1])) + } + + for i, t := range tiles { + if !t.IsValid() { + return p.EmitErr(fmt.Errorf("level contains invalid Tile at (%d, %d)", i, c.level.Height)) + } + } + for i, e := range entities { + if !e.IsValid() { + return p.EmitErr(fmt.Errorf("level contains invalid entity type at (%d, %d)", i, c.level.Height)) + } + } + + c.level.Height++ + c.level.Tiles = append(c.level.Tiles, tiles...) + c.level.Entities = append(c.level.Entities, entities...) + + return c.parseRow +}