Moved level to separate package (soko).

Moved lineparser to gut package.
This commit is contained in:
Sander Schobers 2020-01-15 19:03:52 +01:00
parent a19d33cb9f
commit a4058df9c8
10 changed files with 305 additions and 288 deletions

View File

@ -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}}
}

View File

@ -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
}

View File

@ -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"))
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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"))
}

66
gut/lineparser.go Normal file
View File

@ -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
}

62
soko/level.go Normal file
View File

@ -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
}

85
soko/levelparser.go Normal file
View File

@ -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
}