Compare commits
No commits in common. "master" and "alpha_2" have entirely different histories.
@ -14,8 +14,9 @@ type moveAnimation struct {
|
|||||||
pos geom.PointF32
|
pos geom.PointF32
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMoveAnimation(e *entity, from, to geom.Point) *moveAnimation {
|
func newMoveAnimation(e *entity, to geom.Point) *moveAnimation {
|
||||||
ani := &moveAnimation{e: e, from: from, to: to, pos: from.ToF32()}
|
ani := &moveAnimation{e: e, from: e.pos, to: to, pos: e.pos.ToF32()}
|
||||||
|
ani.e.pos = to
|
||||||
return ani
|
return ani
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
"opslag.de/schobers/krampus19/alui"
|
"opslag.de/schobers/krampus19/alui"
|
||||||
|
|
||||||
@ -24,10 +23,9 @@ func newTexture(bmp *allg5.Bitmap) texture {
|
|||||||
type Context struct {
|
type Context struct {
|
||||||
DisplaySize geom.Point
|
DisplaySize geom.Point
|
||||||
Resources vfs.CopyDir
|
Resources vfs.CopyDir
|
||||||
ResourcesFs afero.Fs
|
|
||||||
Textures map[string]texture
|
Textures map[string]texture
|
||||||
Sprites map[string]sprite
|
Sprites map[string]sprite
|
||||||
Levels *levelPacks
|
Levels map[string]levelPack
|
||||||
Progress progress
|
Progress progress
|
||||||
SpriteDrawer SpriteDrawer
|
SpriteDrawer SpriteDrawer
|
||||||
Settings settings
|
Settings settings
|
||||||
|
@ -2,12 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
"opslag.de/schobers/krampus19/soko"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type entity struct {
|
type entity struct {
|
||||||
id int
|
typ entityType
|
||||||
typ soko.EntityType
|
pos geom.Point
|
||||||
scr entityLoc
|
scr entityLoc
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,6 +15,6 @@ type entityLoc struct {
|
|||||||
z float32
|
z float32
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEntity(e soko.Entity, pos geom.Point) *entity {
|
func newEntity(typ entityType, pos geom.Point) *entity {
|
||||||
return &entity{e.ID, e.Typ, entityLoc{pos.ToF32(), 0}}
|
return &entity{typ, pos, entityLoc{pos.ToF32(), 0}}
|
||||||
}
|
}
|
||||||
|
35
cmd/krampus19/entitylist.go
Normal file
35
cmd/krampus19/entitylist.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "opslag.de/schobers/geom"
|
||||||
|
|
||||||
|
type entityList []*entity
|
||||||
|
|
||||||
|
func (l entityList) Add(e *entity) entityList {
|
||||||
|
return append(l, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l entityList) AddList(list entityList) entityList {
|
||||||
|
return append(l, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l entityList) Find(pos geom.Point) int {
|
||||||
|
for i, e := range l {
|
||||||
|
if e.pos == pos {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l entityList) FindEntity(pos geom.Point) *entity {
|
||||||
|
idx := l.Find(pos)
|
||||||
|
if idx == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return l[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l entityList) Remove(pos geom.Point) entityList {
|
||||||
|
idx := l.Find(pos)
|
||||||
|
return append(l[:idx], l[idx+1:]...)
|
||||||
|
}
|
@ -1,25 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "sort"
|
|
||||||
|
|
||||||
type entityMap map[int]*entity
|
|
||||||
|
|
||||||
func (m entityMap) RenderOrder() []*entity {
|
|
||||||
entities := make([]*entity, 0, len(m))
|
|
||||||
for _, e := range m {
|
|
||||||
entities = append(entities, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(entities, func(i, j int) bool {
|
|
||||||
var posI, posJ = entities[i].scr.pos, entities[j].scr.pos
|
|
||||||
if posI.Y == posJ.Y {
|
|
||||||
if posI.X == posJ.X {
|
|
||||||
return entities[i].id < entities[j].id
|
|
||||||
}
|
|
||||||
return posI.X < posJ.X
|
|
||||||
}
|
|
||||||
return posI.Y < posJ.Y
|
|
||||||
})
|
|
||||||
|
|
||||||
return entities
|
|
||||||
}
|
|
@ -6,11 +6,7 @@ import (
|
|||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"opslag.de/schobers/allg5"
|
"opslag.de/schobers/allg5"
|
||||||
"opslag.de/schobers/fs/vfs"
|
"opslag.de/schobers/fs/vfs"
|
||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
@ -102,60 +98,27 @@ func (g *game) loadTextures(pathToName map[string]string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *game) loadLevelPacks() error {
|
func (g *game) loadLevelPack(ids ...string) error {
|
||||||
g.ctx.Levels = newLevelPacks()
|
g.ctx.Levels = map[string]levelPack{}
|
||||||
|
|
||||||
var ids []string
|
|
||||||
const root = "levels"
|
|
||||||
err := afero.Walk(g.ctx.ResourcesFs, root, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
name := filepath.Base(path)
|
|
||||||
if name == root {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(name, "pack") {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
ids = append(ids, name[4:])
|
|
||||||
return filepath.SkipDir
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
err := g.loadLevelPack(id)
|
fileName := fmt.Sprintf("levels/pack%s.txt", id)
|
||||||
|
f, err := g.ctx.Resources.Open(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer f.Close()
|
||||||
|
pack, err := parseLevelPackAsset(f, func(levelID string) (io.ReadCloser, error) {
|
||||||
|
fileName := fmt.Sprintf("levels/pack%s_level%s.txt", id, levelID)
|
||||||
|
return g.ctx.Resources.Open(fileName)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
g.ctx.Levels[id] = pack
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *game) loadLevelPack(id string) error {
|
|
||||||
fileName := fmt.Sprintf("levels/pack%s/info.txt", id)
|
|
||||||
f, err := g.ctx.Resources.Open(fileName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
pack, err := parseLevelPackAsset(f, func(levelID string) (io.ReadCloser, error) {
|
|
||||||
fileName := fmt.Sprintf("levels/pack%s/level%s.txt", id, levelID)
|
|
||||||
return g.ctx.Resources.Open(fileName)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
g.ctx.Levels.Add(id, pack)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *game) loadSprites(names ...string) error {
|
func (g *game) loadSprites(names ...string) error {
|
||||||
g.ctx.Sprites = map[string]sprite{}
|
g.ctx.Sprites = map[string]sprite{}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
@ -201,12 +164,12 @@ func (g *game) loadAssets() error {
|
|||||||
}
|
}
|
||||||
log.Printf("Loaded %d textures.\n", len(g.ctx.Textures))
|
log.Printf("Loaded %d textures.\n", len(g.ctx.Textures))
|
||||||
|
|
||||||
log.Println("Loading level packs...")
|
log.Println("Loading levels...")
|
||||||
err = g.loadLevelPacks()
|
err = g.loadLevelPack("1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("Loaded %d levels packs (%d levels).\n", g.ctx.Levels.Len(), g.ctx.Levels.LenLevels())
|
log.Printf("Loaded %d levels.\n", len(g.ctx.Levels))
|
||||||
|
|
||||||
log.Println("Loading fonts...")
|
log.Println("Loading fonts...")
|
||||||
err = g.loadFonts()
|
err = g.loadFonts()
|
||||||
@ -229,9 +192,9 @@ func (g *game) Destroy() {
|
|||||||
g.ctx.Destroy()
|
g.ctx.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *game) Init(disp *allg5.Display, settings settings, res vfs.CopyDir, resFs afero.Fs, cons *gut.Console, fps *gut.FPS) error {
|
func (g *game) Init(disp *allg5.Display, settings settings, res vfs.CopyDir, cons *gut.Console, fps *gut.FPS) error {
|
||||||
log.Print("Initializing game...")
|
log.Print("Initializing game...")
|
||||||
g.ctx = &Context{Resources: res, ResourcesFs: resFs, Textures: map[string]texture{}, Settings: settings, Navigation: navigation{game: g}}
|
g.ctx = &Context{Resources: res, Textures: map[string]texture{}, Settings: settings, Navigation: navigation{game: g}}
|
||||||
g.ctx.DisplaySize = geom.Pt(disp.Width(), disp.Height())
|
g.ctx.DisplaySize = geom.Pt(disp.Width(), disp.Height())
|
||||||
g.ctx.SpriteDrawer.ctx = g.ctx
|
g.ctx.SpriteDrawer.ctx = g.ctx
|
||||||
err := g.ctx.Progress.load()
|
err := g.ctx.Progress.load()
|
||||||
|
@ -34,7 +34,7 @@ func userDir() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
dir := filepath.Join(config, "krampus19")
|
dir := filepath.Join(config, "krampus19")
|
||||||
err = os.MkdirAll(dir, 0777)
|
err = os.MkdirAll(dir, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -24,15 +24,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resources() (vfs.CopyDir, afero.Fs, error) {
|
func resources() (vfs.CopyDir, error) {
|
||||||
var embeddedFs = ricefs.NewFs(rice.MustFindBox("res"))
|
var embeddedFs = ricefs.NewFs(rice.MustFindBox("res"))
|
||||||
var osFs = afero.NewBasePathFs(afero.NewOsFs(), "./res")
|
var osFs = afero.NewBasePathFs(afero.NewOsFs(), "./res")
|
||||||
var fs = vfs.NewFallbackFs(osFs, embeddedFs)
|
return vfs.NewCopyDir(vfs.NewFallbackFs(osFs, embeddedFs))
|
||||||
copy, err := vfs.NewCopyDir(fs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return copy, fs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
@ -40,7 +35,7 @@ func run() error {
|
|||||||
cons := &gut.Console{}
|
cons := &gut.Console{}
|
||||||
log.SetOutput(io.MultiWriter(log.Writer(), cons))
|
log.SetOutput(io.MultiWriter(log.Writer(), cons))
|
||||||
|
|
||||||
res, resFs, err := resources()
|
res, err := resources()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -112,7 +107,7 @@ func run() error {
|
|||||||
defer fps.Destroy()
|
defer fps.Destroy()
|
||||||
|
|
||||||
game := &game{}
|
game := &game{}
|
||||||
err = game.Init(disp, settings, res, resFs, cons, fps)
|
err = game.Init(disp, settings, res, cons, fps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
142
cmd/krampus19/level.go
Normal file
142
cmd/krampus19/level.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -4,16 +4,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"opslag.de/schobers/krampus19/gut"
|
|
||||||
|
|
||||||
"opslag.de/schobers/krampus19/soko"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type levelPack struct {
|
type levelPack struct {
|
||||||
name string
|
name string
|
||||||
order []string
|
order []string
|
||||||
levels map[string]soko.Level
|
levels map[string]level
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p levelPack) find(level string) int {
|
func (p levelPack) find(level string) int {
|
||||||
@ -33,8 +29,6 @@ func (p levelPack) FindNext(level string) (string, bool) {
|
|||||||
return p.order[idx+1], true
|
return p.order[idx+1], true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p levelPack) Len() int { return len(p.order) }
|
|
||||||
|
|
||||||
type parseLevelPackContext struct {
|
type parseLevelPackContext struct {
|
||||||
name string
|
name string
|
||||||
levels []string
|
levels []string
|
||||||
@ -42,19 +36,19 @@ type parseLevelPackContext struct {
|
|||||||
|
|
||||||
func parseLevelPackAsset(r io.Reader, openLevelFn func(id string) (io.ReadCloser, error)) (levelPack, error) {
|
func parseLevelPackAsset(r io.Reader, openLevelFn func(id string) (io.ReadCloser, error)) (levelPack, error) {
|
||||||
ctx := &parseLevelPackContext{}
|
ctx := &parseLevelPackContext{}
|
||||||
err := gut.ParseLines(r, ctx.Parse)
|
err := parseLines(r, ctx.Parse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return levelPack{}, err
|
return levelPack{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pack := levelPack{name: ctx.name, levels: map[string]soko.Level{}}
|
pack := levelPack{name: ctx.name, levels: map[string]level{}}
|
||||||
for _, id := range ctx.levels {
|
for _, id := range ctx.levels {
|
||||||
rc, err := openLevelFn(id)
|
rc, err := openLevelFn(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return levelPack{}, err
|
return levelPack{}, err
|
||||||
}
|
}
|
||||||
defer rc.Close()
|
defer rc.Close()
|
||||||
level, err := soko.ParseLevel(rc)
|
level, err := parseLevelAsset(rc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return levelPack{}, err
|
return levelPack{}, err
|
||||||
}
|
}
|
||||||
@ -65,65 +59,65 @@ func parseLevelPackAsset(r io.Reader, openLevelFn func(id string) (io.ReadCloser
|
|||||||
return pack, nil
|
return pack, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *parseLevelPackContext) Parse(p *gut.LineParser) gut.ParseLineFn {
|
func (c *parseLevelPackContext) Parse(p *lineParser) parseLineFn {
|
||||||
if p.SkipSpaceEOF() {
|
if p.skipSpaceEOF() {
|
||||||
return p.EmitErr(errors.New("empty level pack"))
|
return p.emitErr(errors.New("empty level pack"))
|
||||||
}
|
}
|
||||||
return c.parse
|
return c.parse
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *parseLevelPackContext) parse(p *gut.LineParser) gut.ParseLineFn {
|
func (c *parseLevelPackContext) parse(p *lineParser) parseLineFn {
|
||||||
const levelsTag = "levels:"
|
const levelsTag = "levels:"
|
||||||
const nameTag = "name:"
|
const nameTag = "name:"
|
||||||
|
|
||||||
if p.SkipSpaceEOF() {
|
if p.skipSpaceEOF() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
line := p.Next()
|
line := p.next()
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, nameTag):
|
case strings.HasPrefix(line, nameTag):
|
||||||
c.name = strings.TrimSpace(line[len(nameTag):])
|
c.name = strings.TrimSpace(line[len(nameTag):])
|
||||||
return c.parse
|
return c.parse
|
||||||
case strings.HasPrefix(line, levelsTag):
|
case strings.HasPrefix(line, levelsTag):
|
||||||
return gut.SkipSpaceBeforeContent(c.parseLevels)
|
return skipSpaceBeforeContent(c.parseLevels)
|
||||||
}
|
}
|
||||||
return p.EmitErr(errors.New("tag not allowed"))
|
return p.emitErr(errors.New("tag not allowed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *parseLevelPackContext) parseLevels(p *gut.LineParser) gut.ParseLineFn {
|
func (c *parseLevelPackContext) parseLevels(p *lineParser) parseLineFn {
|
||||||
const levelTag = "level:"
|
const levelTag = "level:"
|
||||||
const levelsEndTag = ":levels"
|
const levelsEndTag = ":levels"
|
||||||
|
|
||||||
line := p.Next()
|
line := p.next()
|
||||||
switch line {
|
switch line {
|
||||||
case levelTag:
|
case levelTag:
|
||||||
return gut.SkipSpaceBeforeContent(c.parseLevel)
|
return skipSpaceBeforeContent(c.parseLevel)
|
||||||
case levelsEndTag:
|
case levelsEndTag:
|
||||||
return c.parse
|
return c.parse
|
||||||
}
|
}
|
||||||
return p.EmitErr(errors.New("tag not allowed"))
|
return p.emitErr(errors.New("tag not allowed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *parseLevelPackContext) parseLevel(p *gut.LineParser) gut.ParseLineFn {
|
func (c *parseLevelPackContext) parseLevel(p *lineParser) parseLineFn {
|
||||||
const idTag = "id:"
|
const idTag = "id:"
|
||||||
|
|
||||||
line := p.Next()
|
line := p.next()
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, idTag):
|
case strings.HasPrefix(line, idTag):
|
||||||
c.levels = append(c.levels, strings.TrimSpace(line[len(idTag):]))
|
c.levels = append(c.levels, strings.TrimSpace(line[len(idTag):]))
|
||||||
return gut.SkipSpaceBeforeContent(c.parseLevelEnd)
|
return 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 *gut.LineParser) gut.ParseLineFn {
|
func (c *parseLevelPackContext) parseLevelEnd(p *lineParser) parseLineFn {
|
||||||
const levelEndTag = ":level"
|
const levelEndTag = ":level"
|
||||||
|
|
||||||
line := p.Next()
|
line := p.next()
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, levelEndTag):
|
case strings.HasPrefix(line, levelEndTag):
|
||||||
return gut.SkipSpaceBeforeContent(c.parseLevels)
|
return skipSpaceBeforeContent(c.parseLevels)
|
||||||
}
|
}
|
||||||
return p.EmitErr(errors.New("tag not allowed"))
|
return p.emitErr(errors.New("tag not allowed"))
|
||||||
}
|
}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
type levelPacks struct {
|
|
||||||
packs map[string]levelPack
|
|
||||||
order []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLevelPacks() *levelPacks {
|
|
||||||
return &levelPacks{map[string]levelPack{}, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *levelPacks) Add(id string, pack levelPack) {
|
|
||||||
p.order = append(p.order, id)
|
|
||||||
p.packs[id] = pack
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *levelPacks) ByID(id string) levelPack { return p.packs[id] }
|
|
||||||
|
|
||||||
func (p *levelPacks) FirstPackID() string {
|
|
||||||
if p.Len() == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return p.order[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *levelPacks) Len() int { return len(p.order) }
|
|
||||||
|
|
||||||
func (p *levelPacks) LenLevels() int {
|
|
||||||
var cnt int
|
|
||||||
for _, pack := range p.packs {
|
|
||||||
cnt += pack.Len()
|
|
||||||
}
|
|
||||||
return cnt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *levelPacks) MustSelect() bool { return p.Len() != 1 }
|
|
@ -1,40 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"opslag.de/schobers/allg5"
|
|
||||||
|
|
||||||
"opslag.de/schobers/krampus19/alui"
|
|
||||||
)
|
|
||||||
|
|
||||||
type levelPackSelect struct {
|
|
||||||
alui.Menu
|
|
||||||
|
|
||||||
ctx *Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *levelPackSelect) Enter(ctx *Context) error {
|
|
||||||
s.ctx = ctx
|
|
||||||
s.Init()
|
|
||||||
s.AddChild(newHeader("Select a level pack"))
|
|
||||||
for _, id := range s.ctx.Levels.order {
|
|
||||||
var copy = id
|
|
||||||
s.Add(s.ctx.Levels.ByID(id).name, func() {
|
|
||||||
s.ctx.Navigation.SelectLevel(copy)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
s.Add("Back to main menu", func() { s.ctx.Navigation.ShowMainMenu() })
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *levelPackSelect) Handle(e allg5.Event) {
|
|
||||||
switch e := e.(type) {
|
|
||||||
case *allg5.KeyDownEvent:
|
|
||||||
switch e.KeyCode {
|
|
||||||
case allg5.KeyEscape:
|
|
||||||
s.ctx.Navigation.ShowMainMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.Menu.Handle(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *levelPackSelect) Leave() {}
|
|
@ -19,7 +19,7 @@ type levelSelect struct {
|
|||||||
|
|
||||||
func (s *levelSelect) Enter(ctx *Context) error {
|
func (s *levelSelect) Enter(ctx *Context) error {
|
||||||
s.ctx = ctx
|
s.ctx = ctx
|
||||||
s.pack = s.ctx.Levels.ByID(s.packID)
|
s.pack = s.ctx.Levels[s.packID]
|
||||||
s.Init()
|
s.Init()
|
||||||
name := func(i int, steps int) string {
|
name := func(i int, steps int) string {
|
||||||
if steps == 0 {
|
if steps == 0 {
|
||||||
@ -27,20 +27,12 @@ func (s *levelSelect) Enter(ctx *Context) error {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("Level %d (%d)", i, steps)
|
return fmt.Sprintf("Level %d (%d)", i, steps)
|
||||||
}
|
}
|
||||||
if s.ctx.Levels.MustSelect() {
|
|
||||||
s.AddChild(newHeader(fmt.Sprintf("Select a '%s' level", s.pack.name)))
|
|
||||||
} else {
|
|
||||||
s.AddChild(newHeader("Select a level"))
|
|
||||||
}
|
|
||||||
for i, id := range s.pack.order {
|
for i, id := range s.pack.order {
|
||||||
levelID := id
|
levelID := id
|
||||||
s.Add(name(i+1, s.ctx.Progress.Level(s.packID, levelID).Steps), func() {
|
s.Add(name(i+1, s.ctx.Progress.Level(s.packID, levelID).Steps), func() {
|
||||||
s.ctx.Navigation.PlayLevel(s.packID, levelID)
|
s.ctx.Navigation.PlayLevel(s.packID, levelID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if s.ctx.Levels.MustSelect() {
|
|
||||||
s.Add("Select level pack", func() { s.ctx.Navigation.SelectPack() })
|
|
||||||
}
|
|
||||||
s.Add("Back to main menu", func() { s.ctx.Navigation.ShowMainMenu() })
|
s.Add("Back to main menu", func() { s.ctx.Navigation.ShowMainMenu() })
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
64
cmd/krampus19/lineparser.go
Normal file
64
cmd/krampus19/lineparser.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -11,15 +11,11 @@ type mainMenu struct {
|
|||||||
ctx *Context
|
ctx *Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHeader(label string) alui.Control {
|
|
||||||
return alui.NewMargins(&alui.Label{ControlBase: alui.ControlBase{Font: "header"}, Text: label, TextAlign: allg5.AlignCenter}, 3*margin)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mainMenu) Enter(ctx *Context) error {
|
func (m *mainMenu) Enter(ctx *Context) error {
|
||||||
m.ctx = ctx
|
m.ctx = ctx
|
||||||
m.Init()
|
m.Init()
|
||||||
m.AddChild(newHeader("Sokodragon"))
|
m.AddChild(alui.NewMargins(&alui.Label{ControlBase: alui.ControlBase{Font: "header"}, Text: "Sokodragon", TextAlign: allg5.AlignCenter}, 3*margin))
|
||||||
m.Add("Play", func() { m.ctx.Navigation.SelectPack() })
|
m.Add("Play", func() { m.ctx.Navigation.SelectLevel("1") })
|
||||||
m.Add("Settings", func() { m.ctx.Navigation.ChangeSettings() })
|
m.Add("Settings", func() { m.ctx.Navigation.ChangeSettings() })
|
||||||
m.Add("Quit", func() { m.ctx.Navigation.Quit() })
|
m.Add("Quit", func() { m.ctx.Navigation.Quit() })
|
||||||
return nil
|
return nil
|
||||||
|
@ -28,15 +28,6 @@ func (n *navigation) SelectLevel(packID string) {
|
|||||||
n.switchTo(&levelSelect{packID: packID})
|
n.switchTo(&levelSelect{packID: packID})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *navigation) SelectPack() {
|
|
||||||
if n.game.ctx.Levels.MustSelect() {
|
|
||||||
n.switchTo(&levelPackSelect{})
|
|
||||||
} else {
|
|
||||||
packID := n.game.ctx.Levels.FirstPackID()
|
|
||||||
n.SelectLevel(packID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *navigation) ShowMainMenu() {
|
func (n *navigation) ShowMainMenu() {
|
||||||
n.switchTo(&mainMenu{})
|
n.switchTo(&mainMenu{})
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"sort"
|
||||||
|
|
||||||
"opslag.de/schobers/allg5"
|
"opslag.de/schobers/allg5"
|
||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
"opslag.de/schobers/krampus19/alui"
|
"opslag.de/schobers/krampus19/alui"
|
||||||
"opslag.de/schobers/krampus19/soko"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type playLevel struct {
|
type playLevel struct {
|
||||||
@ -26,24 +25,21 @@ type playLevel struct {
|
|||||||
offset geom.PointF32
|
offset geom.PointF32
|
||||||
scale float32
|
scale float32
|
||||||
|
|
||||||
state playLevelState
|
state playLevelState
|
||||||
pathFinding bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type keyPressedState map[allg5.Key]bool
|
type keyPressedState map[allg5.Key]bool
|
||||||
|
|
||||||
func (s keyPressedState) ArePressed(keys ...allg5.Key) []allg5.Key {
|
func (s keyPressedState) CountPressed(keys ...allg5.Key) int {
|
||||||
pressed := make([]allg5.Key, 0, len(keys))
|
var cnt int
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
if s[k] {
|
if s[k] {
|
||||||
pressed = append(pressed, k)
|
cnt++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pressed
|
return cnt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s keyPressedState) CountPressed(keys ...allg5.Key) int { return len(s.ArePressed(keys...)) }
|
|
||||||
|
|
||||||
func (l *playLevel) Enter(ctx *Context) error {
|
func (l *playLevel) Enter(ctx *Context) error {
|
||||||
l.ctx = ctx
|
l.ctx = ctx
|
||||||
|
|
||||||
@ -86,9 +82,6 @@ func (l *playLevel) onComplete() {
|
|||||||
if ok {
|
if ok {
|
||||||
menu.Add("Continue with next", func() { l.ctx.Navigation.PlayLevel(l.packID, nextID) })
|
menu.Add("Continue with next", func() { l.ctx.Navigation.PlayLevel(l.packID, nextID) })
|
||||||
}
|
}
|
||||||
if l.ctx.Levels.MustSelect() {
|
|
||||||
menu.Add("Select level pack", func() { l.ctx.Navigation.SelectPack() })
|
|
||||||
}
|
|
||||||
menu.Add("Select level", func() { l.ctx.Navigation.SelectLevel(l.packID) })
|
menu.Add("Select level", func() { l.ctx.Navigation.SelectLevel(l.packID) })
|
||||||
menu.Add("Go to main menu", func() { l.ctx.Navigation.ShowMainMenu() })
|
menu.Add("Go to main menu", func() { l.ctx.Navigation.ShowMainMenu() })
|
||||||
|
|
||||||
@ -123,13 +116,13 @@ func (l *playLevel) Layout(ctx *alui.Context, bounds geom.RectangleF32) {
|
|||||||
l.offset = geom.PointF32{}
|
l.offset = geom.PointF32{}
|
||||||
|
|
||||||
level := l.state.Level()
|
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)
|
var content = geom.RectF32(contentCenter.X, contentCenter.Y, contentCenter.X, contentCenter.Y)
|
||||||
for idx, tile := range l.state.Level().Tiles {
|
for idx, tile := range l.state.Level().tiles {
|
||||||
if tile == soko.TileTypeNothing || tile == soko.TileTypeInvalid {
|
if tile == tileNothing || tile == tileInvalid {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pos := level.IdxToPos(idx).ToF32()
|
pos := level.idxToPos(idx).ToF32()
|
||||||
bottomLeft := l.posToScreenF32(pos.Add2D(-1.5, 1.5), 100)
|
bottomLeft := l.posToScreenF32(pos.Add2D(-1.5, 1.5), 100)
|
||||||
content.Min = geom.MinPtF32(content.Min, bottomLeft)
|
content.Min = geom.MinPtF32(content.Min, bottomLeft)
|
||||||
content.Max = geom.MaxPtF32(content.Max, bottomLeft)
|
content.Max = geom.MaxPtF32(content.Max, bottomLeft)
|
||||||
@ -168,10 +161,15 @@ func (l *playLevel) Handle(e allg5.Event) {
|
|||||||
case allg5.KeyEscape:
|
case allg5.KeyEscape:
|
||||||
l.showMenu = true
|
l.showMenu = true
|
||||||
l.menu.Activate(0)
|
l.menu.Activate(0)
|
||||||
case allg5.KeyF5:
|
case l.ctx.Settings.Controls.MoveUp:
|
||||||
l.pathFinding = !l.pathFinding
|
l.state.TryPlayerMove(geom.Pt(0, -1), e.KeyCode)
|
||||||
|
case l.ctx.Settings.Controls.MoveRight:
|
||||||
|
l.state.TryPlayerMove(geom.Pt(1, 0), e.KeyCode)
|
||||||
|
case l.ctx.Settings.Controls.MoveDown:
|
||||||
|
l.state.TryPlayerMove(geom.Pt(0, 1), e.KeyCode)
|
||||||
|
case l.ctx.Settings.Controls.MoveLeft:
|
||||||
|
l.state.TryPlayerMove(geom.Pt(-1, 0), e.KeyCode)
|
||||||
}
|
}
|
||||||
l.state.TryPlayerMove(e.KeyCode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -195,19 +193,19 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, t := range level.Tiles {
|
for i, t := range level.tiles {
|
||||||
pos := geom.Pt(i%level.Width, i/level.Width)
|
pos := geom.Pt(i%level.width, i/level.width)
|
||||||
scr := entityLoc{pos.ToF32(), 0}
|
scr := entityLoc{pos.ToF32(), 0}
|
||||||
switch t {
|
switch t {
|
||||||
case soko.TileTypeBasic:
|
case tileBasic:
|
||||||
if l.state.IsNextToMagma(pos) {
|
if l.state.IsNextToMagma(pos) {
|
||||||
l.drawSprite("lava_brick", "magma", scr)
|
l.drawSprite("lava_brick", "magma", scr)
|
||||||
} else {
|
} else {
|
||||||
l.drawSprite("lava_brick", "lava_brick", scr)
|
l.drawSprite("lava_brick", "lava_brick", scr)
|
||||||
}
|
}
|
||||||
case soko.TileTypeMagma:
|
case tileMagma:
|
||||||
l.drawSprite("magma", "magma", scr)
|
l.drawSprite("magma", "magma", scr)
|
||||||
brick := l.state.FindSunkenBrickEntity(pos)
|
brick := l.state.FindSunkenBrick(pos)
|
||||||
if brick != nil {
|
if brick != nil {
|
||||||
behind, front := splitParticles(scr.pos.Y, l.state.Particles(pos))
|
behind, front := splitParticles(scr.pos.Y, l.state.Particles(pos))
|
||||||
drawParticles(behind)
|
drawParticles(behind)
|
||||||
@ -218,30 +216,26 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entities := l.state.Entities().RenderOrder()
|
entities := l.state.Entities()
|
||||||
|
sort.Slice(entities, func(i, j int) bool {
|
||||||
|
if entities[i].scr.pos.Y == entities[j].scr.pos.Y {
|
||||||
|
return entities[i].scr.pos.X < entities[j].scr.pos.X
|
||||||
|
}
|
||||||
|
return entities[i].scr.pos.Y < entities[j].scr.pos.Y
|
||||||
|
})
|
||||||
|
|
||||||
for _, e := range entities {
|
for _, e := range entities {
|
||||||
switch e.typ {
|
switch e.typ {
|
||||||
case soko.EntityTypeBrick:
|
case entityTypeBrick:
|
||||||
l.drawSprite("brick", "brick", e.scr)
|
l.drawSprite("brick", "brick", e.scr)
|
||||||
case soko.EntityTypePlayer:
|
case entityTypeCharacter:
|
||||||
l.drawSprite("dragon", "dragon", e.scr)
|
l.drawSprite("dragon", "dragon", e.scr)
|
||||||
case soko.EntityTypeTarget:
|
case entityTypeEgg:
|
||||||
l.drawSprite("egg", "egg", e.scr)
|
l.drawSprite("egg", "egg", e.scr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
font := ctx.Fonts.Get("default")
|
font := ctx.Fonts.Get("default")
|
||||||
|
|
||||||
if l.pathFinding {
|
|
||||||
dists := soko.NewPathFinder(&l.state.state).FindDistances()
|
|
||||||
for i := range level.Tiles {
|
|
||||||
pos := geom.Pt(i%level.Width, i/level.Width)
|
|
||||||
scr := entityLoc{pos.ToF32(), 0}
|
|
||||||
posDist := l.posToScreenF32(scr.pos, -20)
|
|
||||||
ctx.Fonts.DrawAlignFont(font, posDist.X, posDist.Y, posDist.X, ctx.Palette.Text, allg5.AlignCenter, strconv.Itoa(dists[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
steps := fmt.Sprintf("STEPS: %d", l.state.Steps())
|
steps := fmt.Sprintf("STEPS: %d", l.state.Steps())
|
||||||
ctx.Fonts.DrawAlignFont(font, bounds.Min.X, 24, bounds.Max.X, ctx.Palette.Text, allg5.AlignCenter, steps)
|
ctx.Fonts.DrawAlignFont(font, bounds.Min.X, 24, bounds.Max.X, ctx.Palette.Text, allg5.AlignCenter, steps)
|
||||||
|
|
||||||
|
@ -7,19 +7,17 @@ import (
|
|||||||
"opslag.de/schobers/allg5"
|
"opslag.de/schobers/allg5"
|
||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
"opslag.de/schobers/krampus19/gut"
|
"opslag.de/schobers/krampus19/gut"
|
||||||
"opslag.de/schobers/krampus19/soko"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type playLevelState struct {
|
type playLevelState struct {
|
||||||
ctx *Context
|
ctx *Context
|
||||||
|
|
||||||
pack levelPack
|
pack levelPack
|
||||||
level soko.Level
|
level level
|
||||||
state soko.State
|
|
||||||
player *entity
|
player *entity
|
||||||
egg *entity
|
egg *entity
|
||||||
bricks entityMap
|
bricks entityList
|
||||||
sunken entityMap
|
sunken entityList
|
||||||
splash map[geom.Point]*splashAnimation
|
splash map[geom.Point]*splashAnimation
|
||||||
|
|
||||||
steps int
|
steps int
|
||||||
@ -31,14 +29,9 @@ type playLevelState struct {
|
|||||||
keysDown keyPressedState
|
keysDown keyPressedState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *playLevelState) Entities() entityMap {
|
func (s *playLevelState) Entities() entityList {
|
||||||
entities := entityMap{}
|
var entities entityList
|
||||||
entities[s.player.id] = s.player
|
return entities.Add(s.player).Add(s.egg).AddList(s.bricks)
|
||||||
entities[s.egg.id] = s.egg
|
|
||||||
for id, e := range s.bricks {
|
|
||||||
entities[id] = e
|
|
||||||
}
|
|
||||||
return entities
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *playLevelState) Particles(at geom.Point) []particle {
|
func (s *playLevelState) Particles(at geom.Point) []particle {
|
||||||
@ -55,48 +48,39 @@ func (s *playLevelState) Particles(at geom.Point) []particle {
|
|||||||
return particles
|
return particles
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *playLevelState) FindSunkenBrickEntity(pos geom.Point) *entity {
|
func (s *playLevelState) FindSunkenBrick(pos geom.Point) *entity {
|
||||||
idx := s.level.PosToIdx(pos)
|
return s.sunken.FindEntity(pos)
|
||||||
if idx == -1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
id := s.state.SunkenBricks[idx]
|
|
||||||
if id != -1 {
|
|
||||||
return s.sunken[id]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *playLevelState) IsNextToMagma(pos geom.Point) bool {
|
func (s *playLevelState) IsNextToMagma(pos geom.Point) bool {
|
||||||
return s.state.Any(soko.IsMagma, soko.Neighbours(pos)...)
|
return s.checkTile(pos.Add2D(1, 0), s.isMagma) ||
|
||||||
|
s.checkTile(pos.Add2D(-1, 0), s.isMagma) ||
|
||||||
|
s.checkTile(pos.Add2D(0, -1), s.isMagma) ||
|
||||||
|
s.checkTile(pos.Add2D(0, 1), s.isMagma)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *playLevelState) Init(ctx *Context, pack, level string, onComplete func()) {
|
func (s *playLevelState) Init(ctx *Context, pack, level string, onComplete func()) {
|
||||||
s.ctx = ctx
|
s.ctx = ctx
|
||||||
s.pack = ctx.Levels.ByID(pack)
|
s.pack = ctx.Levels[pack]
|
||||||
s.level = s.pack.levels[level]
|
s.level = s.pack.levels[level]
|
||||||
s.state = s.level.State()
|
|
||||||
s.bricks = nil
|
s.bricks = nil
|
||||||
s.sunken = nil
|
s.sunken = nil
|
||||||
s.splash = map[geom.Point]*splashAnimation{}
|
s.splash = map[geom.Point]*splashAnimation{}
|
||||||
|
for i, e := range s.level.entities {
|
||||||
newEntity := func(e soko.Entity) *entity { return newEntity(e, s.state.IdxToPos[e.Pos]) }
|
switch e {
|
||||||
|
case entityTypeBrick:
|
||||||
s.player = newEntity(s.state.Player)
|
s.bricks = append(s.bricks, newEntity(e, s.level.idxToPos(i)))
|
||||||
s.egg = newEntity(s.state.Target)
|
case entityTypeCharacter:
|
||||||
s.bricks = entityMap{}
|
s.player = newEntity(e, s.level.idxToPos(i))
|
||||||
for _, e := range s.state.Entities {
|
case entityTypeEgg:
|
||||||
if e.Typ != soko.EntityTypeBrick {
|
s.egg = newEntity(e, s.level.idxToPos(i))
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
s.bricks[e.ID] = newEntity(soko.Entity{ID: e.ID, Pos: e.Pos, Typ: soko.EntityTypeBrick})
|
|
||||||
}
|
}
|
||||||
s.sunken = entityMap{}
|
|
||||||
s.keysDown = keyPressedState{}
|
s.keysDown = keyPressedState{}
|
||||||
s.onComplete = onComplete
|
s.onComplete = onComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *playLevelState) Level() soko.Level { return s.level }
|
func (s *playLevelState) Level() level { return s.level }
|
||||||
|
|
||||||
func (s *playLevelState) PressKey(key allg5.Key) {
|
func (s *playLevelState) PressKey(key allg5.Key) {
|
||||||
s.keysDown[key] = true
|
s.keysDown[key] = true
|
||||||
@ -112,77 +96,114 @@ func (s *playLevelState) Tick(now time.Duration) {
|
|||||||
s.ani.Animate(now)
|
s.ani.Animate(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *playLevelState) TryPlayerMove(key allg5.Key) {
|
func (s *playLevelState) TryPlayerMove(dir geom.Point, key allg5.Key) {
|
||||||
var dir soko.Direction
|
if s.player.scr.pos != s.player.pos.ToF32() {
|
||||||
switch key {
|
|
||||||
case s.ctx.Settings.Controls.MoveUp:
|
|
||||||
dir = soko.DirectionUp
|
|
||||||
case s.ctx.Settings.Controls.MoveRight:
|
|
||||||
dir = soko.DirectionRight
|
|
||||||
case s.ctx.Settings.Controls.MoveDown:
|
|
||||||
dir = soko.DirectionDown
|
|
||||||
case s.ctx.Settings.Controls.MoveLeft:
|
|
||||||
dir = soko.DirectionLeft
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.tryPlayerMove(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *playLevelState) tryPlayerMove(dir soko.Direction) {
|
|
||||||
playerPt := s.state.IdxToPos[s.state.Player.Pos]
|
|
||||||
if s.player.scr.pos != playerPt.ToF32() {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state, ok := s.state.MovePlayer(dir)
|
to := s.player.pos.Add(dir)
|
||||||
if !ok {
|
if !s.canMove(s.player.pos, dir) {
|
||||||
log.Printf("Move is not allowed (tried out move %s)", dir.String())
|
log.Printf("Move is not allowed (tried out move to %s after key '%s' was pressed)", to, gut.KeyToString(key))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
to := state.Player.Pos
|
|
||||||
toPt := state.IdxToPos[to]
|
|
||||||
|
|
||||||
if brickID := s.state.Bricks[to]; brickID != -1 {
|
|
||||||
log.Printf("Brick %d moved", brickID)
|
|
||||||
brick := s.bricks[brickID]
|
|
||||||
brickTo := state.Entities.ByID(brickID).Pos
|
|
||||||
brickToPt := state.IdxToPos[brickTo]
|
|
||||||
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(brick, toPt, brickToPt), func() {
|
|
||||||
log.Printf("Brick movement finished")
|
|
||||||
if sunkenBrickID := state.SunkenBricks[brickTo]; sunkenBrickID == brickID {
|
|
||||||
log.Printf("Sinking brick %d", brickID)
|
|
||||||
delete(s.bricks, brickID)
|
|
||||||
s.sunken[brickID] = brick
|
|
||||||
s.ani.Start(s.ctx.Tick, newSinkAnimation(brick))
|
|
||||||
|
|
||||||
splash := newSplashAnimation(brickToPt)
|
|
||||||
s.splash[brickToPt] = splash
|
|
||||||
s.ani.StartFn(s.ctx.Tick, splash, func() {
|
|
||||||
delete(s.splash, brickToPt)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
s.steps++
|
s.steps++
|
||||||
log.Printf("Moving player to %s", toPt)
|
log.Printf("Moving player to %s", to)
|
||||||
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, playerPt, toPt), func() {
|
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, to), func() {
|
||||||
log.Printf("Player movement finished")
|
log.Printf("Player movement finished")
|
||||||
if to == state.Target.Pos {
|
if s.player.pos == s.egg.pos {
|
||||||
s.complete = true
|
s.complete = true
|
||||||
if onComplete := s.onComplete; onComplete != nil {
|
if onComplete := s.onComplete; onComplete != nil {
|
||||||
onComplete()
|
onComplete()
|
||||||
}
|
}
|
||||||
} else {
|
} else if s.keysDown[key] && s.keysDown.CountPressed(s.ctx.Settings.Controls.MovementKeys()...) == 1 {
|
||||||
pressed := s.keysDown.ArePressed(s.ctx.Settings.Controls.MovementKeys()...)
|
log.Printf("Key %s is still down, moving further", gut.KeyToString(key))
|
||||||
if len(pressed) == 1 {
|
s.TryPlayerMove(dir, key)
|
||||||
key := pressed[0]
|
|
||||||
log.Printf("Movement key %s is down, moving further", gut.KeyToString(key))
|
|
||||||
s.TryPlayerMove(key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
s.state = state
|
|
||||||
|
if brick := s.bricks.FindEntity(to); brick != nil {
|
||||||
|
log.Printf("Pushing brick at %s", to)
|
||||||
|
brickTo := to.Add(dir)
|
||||||
|
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(brick, brickTo), func() {
|
||||||
|
log.Printf("Brick movement finished")
|
||||||
|
if s.checkTile(brickTo, s.wouldBrickSink) {
|
||||||
|
log.Printf("Sinking brick at %s", brickTo)
|
||||||
|
s.bricks = s.bricks.Remove(brickTo)
|
||||||
|
s.sunken = s.sunken.Add(brick)
|
||||||
|
s.ani.Start(s.ctx.Tick, newSinkAnimation(brick))
|
||||||
|
|
||||||
|
splash := newSplashAnimation(brickTo)
|
||||||
|
s.splash[brickTo] = splash
|
||||||
|
s.ani.StartFn(s.ctx.Tick, splash, func() {
|
||||||
|
delete(s.splash, brickTo)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *playLevelState) canMove(from, dir geom.Point) bool {
|
||||||
|
to := from.Add(dir)
|
||||||
|
if !s.checkTile(to, s.isSolidTile) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
brick := s.bricks.FindEntity(to)
|
||||||
|
if brick != nil {
|
||||||
|
brickTo := to.Add(dir)
|
||||||
|
return !s.checkTileNotFound(brickTo, s.isObstructed, true)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *playLevelState) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t 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)
|
||||||
|
if idx == -1 {
|
||||||
|
return notFound
|
||||||
|
}
|
||||||
|
return check(pos, idx, s.level.tiles[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *playLevelState) findEntityAt(pos geom.Point) *entity {
|
||||||
|
if s.player.pos == pos {
|
||||||
|
return s.player
|
||||||
|
}
|
||||||
|
brick := s.bricks.FindEntity(pos)
|
||||||
|
if brick != nil {
|
||||||
|
return brick
|
||||||
|
}
|
||||||
|
return s.sunken.FindEntity(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *playLevelState) isObstructed(pos geom.Point, idx int, t tile) bool {
|
||||||
|
if s.bricks.FindEntity(pos) != nil {
|
||||||
|
return true // brick
|
||||||
|
}
|
||||||
|
switch s.level.tiles[idx] {
|
||||||
|
case tileMagma:
|
||||||
|
return false
|
||||||
|
case tileBasic:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *playLevelState) isMagma(pos geom.Point, idx int, t tile) bool { return t == tileMagma }
|
||||||
|
|
||||||
|
func (s *playLevelState) isSolidTile(pos geom.Point, idx int, t tile) bool {
|
||||||
|
switch t {
|
||||||
|
case tileBasic:
|
||||||
|
return true
|
||||||
|
case 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
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
"opslag.de/schobers/krampus19/gut"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type sprite struct {
|
type sprite struct {
|
||||||
@ -34,7 +33,7 @@ type spritePart struct {
|
|||||||
func loadSpriteAsset(r io.Reader) (sprite, error) {
|
func loadSpriteAsset(r io.Reader) (sprite, error) {
|
||||||
var l sprite
|
var l sprite
|
||||||
ctx := spriteContext{&l, nil}
|
ctx := spriteContext{&l, nil}
|
||||||
err := gut.ParseLines(r, ctx.parse)
|
err := parseLines(r, ctx.parse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sprite{}, err
|
return sprite{}, err
|
||||||
}
|
}
|
||||||
@ -46,44 +45,44 @@ type spriteContext struct {
|
|||||||
part *spritePart
|
part *spritePart
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *spriteContext) parse(p *gut.LineParser) gut.ParseLineFn {
|
func (c *spriteContext) parse(p *lineParser) parseLineFn {
|
||||||
if p.SkipSpaceEOF() {
|
if p.skipSpaceEOF() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
line := p.Peek()
|
line := p.peek()
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, "sprite:"):
|
case strings.HasPrefix(line, "sprite:"):
|
||||||
p.Next()
|
p.next()
|
||||||
return c.parseContent
|
return c.parseContent
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *spriteContext) parseContent(p *gut.LineParser) gut.ParseLineFn {
|
func (c *spriteContext) parseContent(p *lineParser) parseLineFn {
|
||||||
const partTag = "part:"
|
const partTag = "part:"
|
||||||
const textureTag = "texture:"
|
const textureTag = "texture:"
|
||||||
const spriteEndTag = ":sprite"
|
const spriteEndTag = ":sprite"
|
||||||
if p.SkipSpaceEOF() {
|
if p.skipSpaceEOF() {
|
||||||
return p.EmitErr(gut.ErrUnexpectedEnd)
|
return p.emitErr(errUnexpectedEnd)
|
||||||
}
|
}
|
||||||
line := p.Peek()
|
line := p.peek()
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, textureTag):
|
case strings.HasPrefix(line, textureTag):
|
||||||
c.sprite.texture = strings.TrimSpace(line[len(textureTag):])
|
c.sprite.texture = strings.TrimSpace(line[len(textureTag):])
|
||||||
p.Next()
|
p.next()
|
||||||
return c.parseContent
|
return c.parseContent
|
||||||
case line == partTag:
|
case line == partTag:
|
||||||
p.Next()
|
p.next()
|
||||||
c.part = &spritePart{}
|
c.part = &spritePart{}
|
||||||
return c.parsePart
|
return c.parsePart
|
||||||
case line == spriteEndTag:
|
case line == spriteEndTag:
|
||||||
return nil
|
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 *gut.LineParser) gut.ParseLineFn {
|
func (c *spriteContext) parsePart(p *lineParser) parseLineFn {
|
||||||
mustAtois := func(s ...string) []int {
|
mustAtois := func(s ...string) []int {
|
||||||
res := make([]int, len(s))
|
res := make([]int, len(s))
|
||||||
var err error
|
var err error
|
||||||
@ -111,14 +110,14 @@ func (c *spriteContext) parsePart(p *gut.LineParser) gut.ParseLineFn {
|
|||||||
const anchorTag = "anchor:"
|
const anchorTag = "anchor:"
|
||||||
const scaleTag = "scale:"
|
const scaleTag = "scale:"
|
||||||
const partEndTag = ":part"
|
const partEndTag = ":part"
|
||||||
if p.SkipSpaceEOF() {
|
if p.skipSpaceEOF() {
|
||||||
return p.EmitErr(gut.ErrUnexpectedEnd)
|
return p.emitErr(errUnexpectedEnd)
|
||||||
}
|
}
|
||||||
line := p.Peek()
|
line := p.peek()
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(line, nameTag):
|
case strings.HasPrefix(line, nameTag):
|
||||||
c.part.name = strings.TrimSpace(line[len(nameTag):])
|
c.part.name = strings.TrimSpace(line[len(nameTag):])
|
||||||
p.Next()
|
p.next()
|
||||||
return c.parsePart
|
return c.parsePart
|
||||||
case strings.HasPrefix(line, subTextureTag):
|
case strings.HasPrefix(line, subTextureTag):
|
||||||
var coords = mustCoords(line[len(subTextureTag):])
|
var coords = mustCoords(line[len(subTextureTag):])
|
||||||
@ -126,7 +125,7 @@ func (c *spriteContext) parsePart(p *gut.LineParser) gut.ParseLineFn {
|
|||||||
panic("expected four coordinates (min x, min y, size x, size y)")
|
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])
|
c.part.sub = geom.Rect(coords[0], coords[1], coords[0]+coords[2], coords[1]+coords[3])
|
||||||
p.Next()
|
p.next()
|
||||||
return c.parsePart
|
return c.parsePart
|
||||||
case strings.HasPrefix(line, anchorTag):
|
case strings.HasPrefix(line, anchorTag):
|
||||||
var coords = mustCoords(line[len(anchorTag):])
|
var coords = mustCoords(line[len(anchorTag):])
|
||||||
@ -134,16 +133,16 @@ func (c *spriteContext) parsePart(p *gut.LineParser) gut.ParseLineFn {
|
|||||||
panic("expected two coordinates (min x, min y)")
|
panic("expected two coordinates (min x, min y)")
|
||||||
}
|
}
|
||||||
c.part.anchor = geom.Pt(coords[0], coords[1])
|
c.part.anchor = geom.Pt(coords[0], coords[1])
|
||||||
p.Next()
|
p.next()
|
||||||
return c.parsePart
|
return c.parsePart
|
||||||
case strings.HasPrefix(line, scaleTag):
|
case strings.HasPrefix(line, scaleTag):
|
||||||
c.part.scale = mustAtof(line[len(scaleTag):])
|
c.part.scale = mustAtof(line[len(scaleTag):])
|
||||||
p.Next()
|
p.next()
|
||||||
return c.parsePart
|
return c.parsePart
|
||||||
case line == partEndTag:
|
case line == partEndTag:
|
||||||
c.sprite.parts = append(c.sprite.parts, *c.part)
|
c.sprite.parts = append(c.sprite.parts, *c.part)
|
||||||
p.Next()
|
p.next()
|
||||||
return c.parseContent
|
return c.parseContent
|
||||||
}
|
}
|
||||||
return p.EmitErr(errors.New("unexpected content of part"))
|
return p.emitErr(errors.New("unexpected content of part"))
|
||||||
}
|
}
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
import "opslag.de/schobers/geom"
|
|
||||||
|
|
||||||
type Direction int
|
|
||||||
|
|
||||||
const (
|
|
||||||
DirectionUp Direction = iota
|
|
||||||
DirectionRight
|
|
||||||
DirectionDown
|
|
||||||
DirectionLeft
|
|
||||||
)
|
|
||||||
|
|
||||||
var Directions = [4]Direction{DirectionUp, DirectionRight, DirectionDown, DirectionLeft}
|
|
||||||
|
|
||||||
func (d Direction) Invert() Direction {
|
|
||||||
switch d {
|
|
||||||
case DirectionUp:
|
|
||||||
return DirectionDown
|
|
||||||
case DirectionRight:
|
|
||||||
return DirectionLeft
|
|
||||||
case DirectionDown:
|
|
||||||
return DirectionUp
|
|
||||||
case DirectionLeft:
|
|
||||||
return DirectionRight
|
|
||||||
}
|
|
||||||
panic("invalid direction")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Direction) String() string {
|
|
||||||
switch d {
|
|
||||||
case DirectionUp:
|
|
||||||
return "up"
|
|
||||||
case DirectionRight:
|
|
||||||
return "right"
|
|
||||||
case DirectionDown:
|
|
||||||
return "down"
|
|
||||||
case DirectionLeft:
|
|
||||||
return "left"
|
|
||||||
}
|
|
||||||
return "invalid"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Direction) ToPoint() geom.Point {
|
|
||||||
switch d {
|
|
||||||
case DirectionUp:
|
|
||||||
return geom.Pt(0, -1)
|
|
||||||
case DirectionRight:
|
|
||||||
return geom.Pt(1, 0)
|
|
||||||
case DirectionDown:
|
|
||||||
return geom.Pt(0, 1)
|
|
||||||
case DirectionLeft:
|
|
||||||
return geom.Pt(-1, 0)
|
|
||||||
}
|
|
||||||
return geom.Point{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Neighbours(p geom.Point) []geom.Point {
|
|
||||||
neighbours := make([]geom.Point, 4)
|
|
||||||
for i, dir := range Directions {
|
|
||||||
neighbours[i] = p.Add(dir.ToPoint())
|
|
||||||
}
|
|
||||||
return neighbours
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
type Entity struct {
|
|
||||||
ID int
|
|
||||||
Pos int
|
|
||||||
Typ EntityType
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entities []Entity
|
|
||||||
|
|
||||||
func (e Entities) ByID(id int) Entity {
|
|
||||||
idx := e.IdxById(id)
|
|
||||||
if idx == -1 {
|
|
||||||
return Entity{ID: -1}
|
|
||||||
}
|
|
||||||
return e[idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Entities) Clone() Entities {
|
|
||||||
clone := make(Entities, len(e))
|
|
||||||
copy(clone, e)
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Entities) IdxById(id int) int {
|
|
||||||
for i, e := range e {
|
|
||||||
if e.ID == id {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
type Ints []int
|
|
||||||
|
|
||||||
func NewInts(n int) Ints {
|
|
||||||
ids := make(Ints, n)
|
|
||||||
for i := range ids {
|
|
||||||
ids[i] = -1
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Ints) Clone() Ints {
|
|
||||||
clone := make(Ints, len(e))
|
|
||||||
copy(clone, e)
|
|
||||||
return clone
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
import "opslag.de/schobers/geom"
|
|
||||||
|
|
||||||
type EntityLocations map[geom.Point]int
|
|
||||||
|
|
||||||
func (l EntityLocations) Add(p geom.Point, id int) { l[p] = id }
|
|
||||||
|
|
||||||
func (l EntityLocations) Clone() EntityLocations {
|
|
||||||
clone := EntityLocations{}
|
|
||||||
for p, id := range l {
|
|
||||||
clone[p] = id
|
|
||||||
}
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l EntityLocations) Has(p geom.Point) bool {
|
|
||||||
_, ok := l[p]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l EntityLocations) Move(from, to geom.Point) {
|
|
||||||
id, ok := l[from]
|
|
||||||
if !ok {
|
|
||||||
panic("no entitiy at position")
|
|
||||||
}
|
|
||||||
l.Remove(from)
|
|
||||||
l.Add(to, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l EntityLocations) Remove(p geom.Point) { delete(l, p) }
|
|
@ -1,23 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
type EntityType byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
EntityTypeInvalid EntityType = EntityType(0)
|
|
||||||
EntityTypeNone = '_'
|
|
||||||
EntityTypePlayer = '@'
|
|
||||||
EntityTypeTarget = 'X'
|
|
||||||
EntityTypeBrick = 'B'
|
|
||||||
)
|
|
||||||
|
|
||||||
func (e EntityType) IsValid() bool {
|
|
||||||
switch e {
|
|
||||||
case EntityTypeNone:
|
|
||||||
case EntityTypePlayer:
|
|
||||||
case EntityTypeTarget:
|
|
||||||
case EntityTypeBrick:
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
import (
|
|
||||||
"opslag.de/schobers/geom"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Level struct {
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
|
|
||||||
Tiles []TileType
|
|
||||||
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 (l Level) Moves() []Moves {
|
|
||||||
moves := make([]Moves, len(l.Tiles))
|
|
||||||
w := l.Width
|
|
||||||
for y := 0; y < l.Height; y++ {
|
|
||||||
for x := 0; x < w; x++ {
|
|
||||||
var m Moves
|
|
||||||
idx := y*w + x
|
|
||||||
if y > 0 {
|
|
||||||
m.Valid = append(m.Valid, idx-w)
|
|
||||||
m.All[DirectionUp] = idx - w
|
|
||||||
} else {
|
|
||||||
m.All[DirectionUp] = -1
|
|
||||||
}
|
|
||||||
if y < (l.Height - 1) {
|
|
||||||
m.Valid = append(m.Valid, idx+w)
|
|
||||||
m.All[DirectionDown] = idx + w
|
|
||||||
} else {
|
|
||||||
m.All[DirectionDown] = -1
|
|
||||||
}
|
|
||||||
if x > 0 {
|
|
||||||
m.Valid = append(m.Valid, idx-1)
|
|
||||||
m.All[DirectionLeft] = idx - 1
|
|
||||||
} else {
|
|
||||||
m.All[DirectionLeft] = idx - 1
|
|
||||||
}
|
|
||||||
if x < (w - 1) {
|
|
||||||
m.Valid = append(m.Valid, idx+1)
|
|
||||||
m.All[DirectionRight] = idx + 1
|
|
||||||
} else {
|
|
||||||
m.All[DirectionRight] = idx + 1
|
|
||||||
}
|
|
||||||
moves[idx] = m
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return moves
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Level) MoveIdx(idx int, dir Direction) int {
|
|
||||||
switch dir {
|
|
||||||
case DirectionUp:
|
|
||||||
if idx < l.Width {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return idx - l.Width
|
|
||||||
case DirectionRight:
|
|
||||||
if (idx+1)%l.Width == 0 {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return idx + 1
|
|
||||||
case DirectionDown:
|
|
||||||
if idx >= (l.Width*l.Height)-l.Width {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return idx + l.Width
|
|
||||||
case DirectionLeft:
|
|
||||||
if idx%l.Width == 0 {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return idx - 1
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Level) Size() int { return l.Width * l.Height }
|
|
||||||
|
|
||||||
func (l Level) State() State {
|
|
||||||
return NewState(l)
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
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 []TileType
|
|
||||||
var entities []EntityType
|
|
||||||
for i := 0; i < len(line); i += 2 {
|
|
||||||
tiles = append(tiles, TileType(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 TileType 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
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
import "opslag.de/schobers/geom"
|
|
||||||
|
|
||||||
type Locations map[geom.Point]bool
|
|
||||||
|
|
||||||
func (l Locations) Add(p geom.Point) { l[p] = true }
|
|
||||||
|
|
||||||
func (l Locations) Clone() Locations {
|
|
||||||
clone := Locations{}
|
|
||||||
for p := range l {
|
|
||||||
clone[p] = true
|
|
||||||
}
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Locations) Remove(p geom.Point) { delete(l, p) }
|
|
@ -1,6 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
type Moves struct {
|
|
||||||
All [4]int
|
|
||||||
Valid []int
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PathFinder struct {
|
|
||||||
state *State
|
|
||||||
moves []Moves
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPathFinder(s *State) PathFinder {
|
|
||||||
return PathFinder{s, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPathFinderMoves(s *State, moves []Moves) PathFinder {
|
|
||||||
return PathFinder{s, moves}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p PathFinder) Find(target int) []int {
|
|
||||||
source := p.state.Player.Pos
|
|
||||||
level := p.state.Level
|
|
||||||
size := level.Size()
|
|
||||||
distances := NewInts(size)
|
|
||||||
distances[source] = 0
|
|
||||||
moves := NewInts(size)
|
|
||||||
moves[source] = source
|
|
||||||
state := p.newPathFinderState(source)
|
|
||||||
|
|
||||||
heuristic := func(i int) int {
|
|
||||||
idx := state.frontier[i]
|
|
||||||
return distances[idx] + p.state.IdxToPos[idx].DistInt(p.state.IdxToPos[target])
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
curr := state.frontier[0]
|
|
||||||
if curr == target {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
state.frontier = state.frontier[1:]
|
|
||||||
|
|
||||||
state.findBetterNeighbours(distances, curr, func(nextIdx, newDistance int) {
|
|
||||||
moves[nextIdx] = curr
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(state.frontier) == 0 {
|
|
||||||
return nil // no path
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply heuristic to frontier (favor points closer to target)
|
|
||||||
sort.Slice(state.frontier, func(i, j int) bool { return heuristic(i) < heuristic(j) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// build reverse path
|
|
||||||
curr := target
|
|
||||||
path := []int{curr}
|
|
||||||
for {
|
|
||||||
curr = moves[curr]
|
|
||||||
if curr == source {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
path = append(path, curr)
|
|
||||||
}
|
|
||||||
// reverse path
|
|
||||||
n := len(path)
|
|
||||||
for i := 0; i < n/2; i++ {
|
|
||||||
path[i], path[n-i-1] = path[n-i-1], path[i]
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p PathFinder) FindDistances() Ints {
|
|
||||||
source := p.state.Player.Pos
|
|
||||||
level := p.state.Level
|
|
||||||
size := level.Size()
|
|
||||||
distances := NewInts(size)
|
|
||||||
distances[source] = 0
|
|
||||||
state := p.newPathFinderState(source)
|
|
||||||
|
|
||||||
for {
|
|
||||||
curr := state.frontier[0]
|
|
||||||
state.frontier = state.frontier[1:]
|
|
||||||
|
|
||||||
state.findBetterNeighbours(distances, curr, nil)
|
|
||||||
|
|
||||||
if len(state.frontier) == 0 {
|
|
||||||
return distances
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p PathFinder) newPathFinderState(source int) *pathFinderState {
|
|
||||||
return &pathFinderState{p.state, append(make([]int, 0, p.state.Level.Size()), source), p.moves}
|
|
||||||
}
|
|
||||||
|
|
||||||
type pathFinderState struct {
|
|
||||||
state *State
|
|
||||||
frontier []int
|
|
||||||
moves []Moves
|
|
||||||
}
|
|
||||||
|
|
||||||
type betterNeighbourFn func(int, int)
|
|
||||||
|
|
||||||
func (s *pathFinderState) findBetterNeighbours(distances []int, curr int, better betterNeighbourFn) {
|
|
||||||
currDistance := distances[curr]
|
|
||||||
newDistance := currDistance + 1
|
|
||||||
|
|
||||||
var moves []int
|
|
||||||
if s.moves == nil {
|
|
||||||
for _, dir := range Directions {
|
|
||||||
nextIdx := s.state.Level.MoveIdx(curr, dir)
|
|
||||||
if nextIdx == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
moves = append(moves, nextIdx)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
moves = s.moves[curr].Valid
|
|
||||||
}
|
|
||||||
for _, nextIdx := range moves {
|
|
||||||
if distance := distances[nextIdx]; distance != -1 && distance <= newDistance { // skip when shorter path exists
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !s.state.Walkable[nextIdx] || s.state.Bricks[nextIdx] != -1 { // filter neighbours
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
distances[nextIdx] = newDistance
|
|
||||||
s.frontier = append(s.frontier, nextIdx)
|
|
||||||
if better == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
better(nextIdx, newDistance)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
func Solve(l Level) int {
|
|
||||||
state := l.State()
|
|
||||||
return solve(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func solve(state State) int {
|
|
||||||
level := state.Level
|
|
||||||
target := state.Target.Pos
|
|
||||||
costs := &stateCostQueue{}
|
|
||||||
costs.Put(&stateCost{&state, 0, nil})
|
|
||||||
costsByPlayerIdx := make([][]*stateCost, level.Size())
|
|
||||||
findCost := func(s *State) *stateCost {
|
|
||||||
costs := costsByPlayerIdx[s.Player.Pos]
|
|
||||||
for i := 0; i < len(costs); i++ {
|
|
||||||
if !costs[i].state.Equal(s) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return costs[i]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
addCost := func(next *State, nextCost int) {
|
|
||||||
cost := findCost(next)
|
|
||||||
if cost == nil {
|
|
||||||
newCost := &stateCost{next, nextCost, nil}
|
|
||||||
costs.Put(newCost)
|
|
||||||
player := next.Player.Pos
|
|
||||||
costsByPlayerIdx[player] = append(costsByPlayerIdx[player], newCost)
|
|
||||||
} else if nextCost < cost.cost {
|
|
||||||
cost.cost = nextCost
|
|
||||||
costs.Update(cost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
moves := level.Moves()
|
|
||||||
type direction struct {
|
|
||||||
dir Direction
|
|
||||||
inverse Direction
|
|
||||||
}
|
|
||||||
dirs := make([]direction, 4)
|
|
||||||
for _, dir := range Directions {
|
|
||||||
dirs[dir] = direction{dir, dir.Invert()}
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
curr := costs.Get()
|
|
||||||
currPlayerPos := curr.state.Player.Pos
|
|
||||||
if currPlayerPos == target {
|
|
||||||
return curr.cost
|
|
||||||
}
|
|
||||||
|
|
||||||
if curr.distances == nil {
|
|
||||||
curr.distances = NewPathFinderMoves(curr.state, moves).FindDistances()
|
|
||||||
}
|
|
||||||
distances := curr.distances
|
|
||||||
for _, e := range curr.state.Entities {
|
|
||||||
idx := e.Pos
|
|
||||||
if e.Typ != EntityTypeBrick || curr.state.SunkenBricks[idx] == e.ID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
player := moves[e.Pos].All[dirs[i].inverse]
|
|
||||||
if player == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
walk := distances[player]
|
|
||||||
if walk == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !curr.state.IsOpenForBrick(curr.state.IdxToPos[moves[e.Pos].All[i]]) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
curr.state.Player.Pos = player
|
|
||||||
next, ok := curr.state.MovePlayer(dirs[i].dir)
|
|
||||||
if !ok {
|
|
||||||
panic("should be a valid move")
|
|
||||||
}
|
|
||||||
nextCost := curr.cost + walk + 1
|
|
||||||
addCost(&next, nextCost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
curr.state.Player.Pos = currPlayerPos
|
|
||||||
if walk := distances[target]; walk != -1 {
|
|
||||||
next := curr.state.Clone()
|
|
||||||
next.Player.Pos = target
|
|
||||||
nextCost := curr.cost + walk
|
|
||||||
addCost(&next, nextCost)
|
|
||||||
}
|
|
||||||
|
|
||||||
if costs.Empty() {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
var levelBenchmark = `level:
|
|
||||||
._._._._._._._._._._._._._._._._._._._._
|
|
||||||
._._._._._#_#_#_._._._._._._._._._._._._
|
|
||||||
._._._._._#_#_#_._._._._._._._._._._._._
|
|
||||||
._._._._._#_#_#_._._._._._._._._._._._._
|
|
||||||
._._._#_#_#_#_#_#_._._._._._._._._._._._
|
|
||||||
._._._#_._#_._._#_._._._._#X#_#_~_#_#_._
|
|
||||||
._#_#_#_._#_._._#_._._._._._._._~_#_#_._
|
|
||||||
._#_#B#_#_#B#_#_#_#_#_#_#_#_#_#_#_#_#_._
|
|
||||||
._._._._._#_._._._#_._#@._._._._#_#_._._
|
|
||||||
._._._._._#_#_#_#_#_._._._._._._._._._._
|
|
||||||
._._._._._._._._._._._._._._._._._._._._
|
|
||||||
:level`
|
|
||||||
|
|
||||||
var levelEasy = `level:
|
|
||||||
._._._._._._._._._._
|
|
||||||
._#@#_#_#B#_~_#_#X._
|
|
||||||
._._._._._._._._._._
|
|
||||||
:level`
|
|
||||||
|
|
||||||
var level1 = `level:
|
|
||||||
._._._._._._._._._._
|
|
||||||
._#_#_#_#_~_~_~_#_._
|
|
||||||
._#_#_._#B~_~_#_#_._
|
|
||||||
._#_#_#_#_~_~_~_#_._
|
|
||||||
._#_#_._#B~_~_#_#_._
|
|
||||||
._#@#_#_#_~_~_#_#X._
|
|
||||||
._#_#_~_~_~_#_#_#_._
|
|
||||||
._#_#_~_~_~_#_#_#_._
|
|
||||||
._._._._._._._._._._
|
|
||||||
:level`
|
|
||||||
|
|
||||||
var level2 = `level:
|
|
||||||
._._._._._._._._._._._._._._._._._._._._
|
|
||||||
._._._._._#_#_#_._._._._._._._._._._._._
|
|
||||||
._._._._._#B#_#_._._._._._._._._._._._._
|
|
||||||
._._._._._#_#_#B._._._._._._._._._._._._
|
|
||||||
._._._#_#_#B#_#B#_._._._._._._._._._._._
|
|
||||||
._._._#_._#_._._#_._._._._#X#_~_~_~_#_._
|
|
||||||
._#_#_#_._#_._._#_._._._._._._._~_~_#_._
|
|
||||||
._#_#B#_#_#B#_#_#_#_#_#_#_#_#_#_~_#_#_._
|
|
||||||
._._._._._#_._._._#_._#@._._._._#_#_._._
|
|
||||||
._._._._._#_#_#_#_#_._._._._._._._._._._
|
|
||||||
._._._._._._._._._._._._._._._._._._._._
|
|
||||||
:level`
|
|
||||||
|
|
||||||
var level3 = `level:
|
|
||||||
._._._._._._._._._._
|
|
||||||
._._._._._._._._._._
|
|
||||||
._~_#_#_#_#B#_._._._
|
|
||||||
._~_#B#_#_._#_._._._
|
|
||||||
._~_._#_~_#_#_._._._
|
|
||||||
._#X._#_~_#_._._._._
|
|
||||||
._._#_#B#_#_._._._._
|
|
||||||
._._#@#_._._._._._._
|
|
||||||
._._._._._._._._._._
|
|
||||||
:level`
|
|
||||||
|
|
||||||
var level4 = `level:
|
|
||||||
._._._._._._._._._._._._
|
|
||||||
._._#_#_._._._._._._._._
|
|
||||||
._._#_#B#_#B#@#_#_._._._
|
|
||||||
._._#_#_#_#_#B#_#_._._._
|
|
||||||
._._._._._._#_._._._._._
|
|
||||||
._._._._._._#_._._._._._
|
|
||||||
._._._._._._#B._._._._._
|
|
||||||
._._._#_#_._#_._#_#_._._
|
|
||||||
._#_#_#_#_~_#_#_#_#_#_._
|
|
||||||
._#_._._#_._#_._#_._#_._
|
|
||||||
._#_._#X~_._~_._~_._#_._
|
|
||||||
._#_._._#_._#_._#_._#_._
|
|
||||||
._#_#_#_#_#_~_#_#_#_#_._
|
|
||||||
._._._._._._._._._._._._
|
|
||||||
:level`
|
|
||||||
|
|
||||||
func mustParseLevel(s string) Level {
|
|
||||||
l, err := ParseLevel(bytes.NewBufferString(s))
|
|
||||||
if err != nil {
|
|
||||||
panic("couldn't parse level")
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSolver(t *testing.T) {
|
|
||||||
solve := func(s string) int {
|
|
||||||
l := mustParseLevel(s)
|
|
||||||
steps := Solve(l)
|
|
||||||
return steps
|
|
||||||
}
|
|
||||||
assert.Equal(t, 7, solve(levelEasy))
|
|
||||||
assert.Equal(t, 35, solve(level1))
|
|
||||||
// assert.Equal(t, -1, solve(level2))
|
|
||||||
assert.Equal(t, 49, solve(level3))
|
|
||||||
// assert.Equal(t, -1, solve(level4))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkSolver(b *testing.B) {
|
|
||||||
l := mustParseLevel(levelBenchmark)
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
Solve(l)
|
|
||||||
}
|
|
||||||
}
|
|
240
soko/state.go
240
soko/state.go
@ -1,240 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
import "opslag.de/schobers/geom"
|
|
||||||
|
|
||||||
type State struct {
|
|
||||||
Player Entity
|
|
||||||
Target Entity
|
|
||||||
|
|
||||||
Entities Entities
|
|
||||||
|
|
||||||
Level Level
|
|
||||||
IdxToPos []geom.Point
|
|
||||||
Bricks Ints
|
|
||||||
SunkenBricks Ints
|
|
||||||
Walkable []bool
|
|
||||||
|
|
||||||
nextEntityID int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewState(l Level) State {
|
|
||||||
size := l.Size()
|
|
||||||
s := State{
|
|
||||||
Entities: Entities{},
|
|
||||||
Level: l,
|
|
||||||
IdxToPos: make([]geom.Point, size),
|
|
||||||
Bricks: NewInts(size),
|
|
||||||
SunkenBricks: NewInts(size),
|
|
||||||
Walkable: make([]bool, size),
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx, t := range l.Tiles {
|
|
||||||
switch t {
|
|
||||||
case TileTypeBasic:
|
|
||||||
s.Walkable[idx] = true
|
|
||||||
}
|
|
||||||
s.IdxToPos[idx] = s.Level.IdxToPos(idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx, e := range l.Entities {
|
|
||||||
pos := s.IdxToPos[idx]
|
|
||||||
switch e {
|
|
||||||
case EntityTypeBrick:
|
|
||||||
s.addBrick(pos)
|
|
||||||
case EntityTypePlayer:
|
|
||||||
s.initPlayer(pos)
|
|
||||||
case EntityTypeTarget:
|
|
||||||
s.initTarget(pos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone creates and returns a clone of the state.
|
|
||||||
func (s State) Clone() State {
|
|
||||||
return State{
|
|
||||||
Player: s.Player,
|
|
||||||
Target: s.Target,
|
|
||||||
Entities: s.Entities.Clone(),
|
|
||||||
Level: s.Level,
|
|
||||||
IdxToPos: s.IdxToPos,
|
|
||||||
Bricks: s.Bricks.Clone(),
|
|
||||||
SunkenBricks: s.SunkenBricks.Clone(),
|
|
||||||
Walkable: append(s.Walkable[:0:0], s.Walkable...),
|
|
||||||
nextEntityID: s.nextEntityID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s State) All(pred func(State, geom.Point) bool, p ...geom.Point) bool {
|
|
||||||
for _, p := range p {
|
|
||||||
if !pred(s, p) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s State) Any(pred func(State, geom.Point) bool, p ...geom.Point) bool {
|
|
||||||
for _, p := range p {
|
|
||||||
if pred(s, p) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) Equal(other *State) bool {
|
|
||||||
for i := 0; i < len(s.Entities); i++ {
|
|
||||||
if other.Entities[i].Pos != s.Entities[i].Pos {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s State) IsOpenForBrick(p geom.Point) bool {
|
|
||||||
idx := s.Level.PosToIdx(p)
|
|
||||||
if idx == -1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return s.isOpenForBrick(idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsWalkable indicates that a player could walk over this tile regardless of any Bricks that might be there.
|
|
||||||
func (s State) IsWalkable(p geom.Point) bool {
|
|
||||||
idx := s.Level.PosToIdx(p)
|
|
||||||
if idx == -1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return s.Walkable[idx]
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsWalkableAndFree indicates that a player can walk over this tile (no brick is at the tile).
|
|
||||||
func (s State) IsWalkableAndFree(p geom.Point) bool {
|
|
||||||
idx := s.Level.PosToIdx(p)
|
|
||||||
if idx == -1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return s.Walkable[idx] && s.Bricks[idx] == -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// MovePlayer tries to move the player in the specified direction. Returns the new state and true if the move is valid. Returns the current state and false is the move was invalid.
|
|
||||||
func (s State) MovePlayer(dir Direction) (State, bool) {
|
|
||||||
to := s.Level.MoveIdx(s.Player.Pos, dir)
|
|
||||||
if to == -1 {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.Walkable[to] {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
|
|
||||||
brickID := s.Bricks[to]
|
|
||||||
if brickID == -1 {
|
|
||||||
return s.Mutate(func(s *State) {
|
|
||||||
s.movePlayer(to)
|
|
||||||
}), true
|
|
||||||
}
|
|
||||||
|
|
||||||
brickTo := s.Level.MoveIdx(to, dir)
|
|
||||||
if brickTo == -1 {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
if !s.isOpenForBrick(brickTo) {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
return s.Mutate(func(s *State) {
|
|
||||||
s.movePlayer(to)
|
|
||||||
|
|
||||||
if s.Level.Tiles[brickTo] == TileTypeMagma && s.SunkenBricks[brickTo] == -1 {
|
|
||||||
s.SunkenBricks[brickTo] = brickID
|
|
||||||
s.Walkable[brickTo] = true
|
|
||||||
s.Bricks[to] = -1
|
|
||||||
} else {
|
|
||||||
s.Bricks[brickTo] = brickID
|
|
||||||
s.Bricks[to] = -1
|
|
||||||
}
|
|
||||||
s.updateEntity(brickID, Entity{brickID, brickTo, EntityTypeBrick})
|
|
||||||
}), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s State) SetPlayer(to geom.Point) (State, bool) {
|
|
||||||
toIdx := s.Level.PosToIdx(to)
|
|
||||||
if toIdx == -1 {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
if !s.Walkable[toIdx] {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
if s.Bricks[toIdx] != -1 {
|
|
||||||
return s, false
|
|
||||||
}
|
|
||||||
return s.Mutate(func(s *State) {
|
|
||||||
s.movePlayer(toIdx)
|
|
||||||
}), true
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mutate clones the state, applies the mutation on the clone and returns the clone.
|
|
||||||
func (s State) Mutate(fn func(s *State)) State {
|
|
||||||
clone := s.Clone()
|
|
||||||
fn(&clone)
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) addBrick(pos geom.Point) {
|
|
||||||
s.addEntity(pos, EntityTypeBrick, func(e Entity) {
|
|
||||||
idx := s.Level.PosToIdx(pos)
|
|
||||||
if idx != -1 {
|
|
||||||
s.Bricks[idx] = e.ID
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) addEntity(pos geom.Point, typ EntityType, add func(Entity)) {
|
|
||||||
id := s.nextEntityID
|
|
||||||
e := Entity{id, s.Level.PosToIdx(pos), typ}
|
|
||||||
add(e)
|
|
||||||
s.Entities = append(s.Entities, e)
|
|
||||||
s.nextEntityID++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) initPlayer(pos geom.Point) {
|
|
||||||
s.addEntity(pos, EntityTypePlayer, func(e Entity) {
|
|
||||||
s.Player = e
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) initTarget(pos geom.Point) {
|
|
||||||
s.addEntity(pos, EntityTypeTarget, func(e Entity) {
|
|
||||||
s.Target = e
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s State) isOpenForBrick(idx int) bool {
|
|
||||||
if s.Walkable[idx] {
|
|
||||||
return s.Bricks[idx] == -1
|
|
||||||
}
|
|
||||||
return s.Level.Tiles[idx] == TileTypeMagma
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) updateEntity(id int, e Entity) {
|
|
||||||
idx := s.Entities.IdxById(id)
|
|
||||||
if idx == -1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.Entities[idx] = e
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) movePlayer(to int) {
|
|
||||||
s.Player.Pos = to
|
|
||||||
s.updateEntity(s.Player.ID, Entity{s.Player.ID, to, EntityTypePlayer})
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsMagma(s State, p geom.Point) bool {
|
|
||||||
idx := s.Level.PosToIdx(p)
|
|
||||||
if idx == -1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return s.Level.Tiles[idx] == TileTypeMagma
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
type stateCost struct {
|
|
||||||
state *State
|
|
||||||
cost int
|
|
||||||
distances Ints
|
|
||||||
}
|
|
||||||
|
|
||||||
type stateCostQueue struct {
|
|
||||||
first *stateCostQueueItem
|
|
||||||
}
|
|
||||||
|
|
||||||
type stateCostQueueItem struct {
|
|
||||||
Value *stateCost
|
|
||||||
Next *stateCostQueueItem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *stateCostQueue) Empty() bool {
|
|
||||||
return q.first == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *stateCostQueue) Get() *stateCost {
|
|
||||||
if q.Empty() {
|
|
||||||
panic("nothing in queue")
|
|
||||||
}
|
|
||||||
first := q.first
|
|
||||||
q.first = first.Next
|
|
||||||
return first.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *stateCostQueue) Put(value *stateCost) {
|
|
||||||
priority := value.cost
|
|
||||||
item := &stateCostQueueItem{value, nil}
|
|
||||||
if q.Empty() {
|
|
||||||
q.first = item
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if priority < q.first.Value.cost {
|
|
||||||
item.Next = q.first
|
|
||||||
q.first = item
|
|
||||||
return
|
|
||||||
}
|
|
||||||
curr := q.first
|
|
||||||
for curr.Next != nil {
|
|
||||||
if priority < curr.Next.Value.cost {
|
|
||||||
item.Next = curr.Next
|
|
||||||
curr.Next = item
|
|
||||||
return
|
|
||||||
}
|
|
||||||
curr = curr.Next
|
|
||||||
}
|
|
||||||
curr.Next = item
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *stateCostQueue) Update(value *stateCost) {
|
|
||||||
var prev *stateCostQueueItem
|
|
||||||
curr := q.first
|
|
||||||
for curr.Value != value {
|
|
||||||
prev = curr
|
|
||||||
curr = curr.Next
|
|
||||||
if curr == nil {
|
|
||||||
panic("not in queue")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if prev == nil {
|
|
||||||
q.first = curr.Next
|
|
||||||
} else {
|
|
||||||
prev.Next = curr.Next
|
|
||||||
}
|
|
||||||
q.Put(value)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package soko
|
|
||||||
|
|
||||||
type TileType byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
TileTypeInvalid TileType = TileType(0)
|
|
||||||
TileTypeNothing = '.'
|
|
||||||
TileTypeBasic = '#'
|
|
||||||
TileTypeMagma = '~'
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t TileType) IsValid() bool {
|
|
||||||
switch t {
|
|
||||||
case TileTypeNothing:
|
|
||||||
case TileTypeBasic:
|
|
||||||
case TileTypeMagma:
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user