Compare commits

..

12 Commits

Author SHA1 Message Date
365e9dbbbb Added solver & optimized state.
Positions are kept by index only.
2020-01-16 07:33:04 +01:00
8fc459e1d6 Added path finder & visualization (F5). 2020-01-15 21:49:01 +01:00
93be82bdbd Moved level state to soko package.
Renamed entity type character and egg to player and target respectively.
2020-01-15 21:25:32 +01:00
1e9d3e9089 Renamed Tile to TileType. 2020-01-15 19:51:57 +01:00
cb81ced655 Moved tile & entity type to separate code unit. 2020-01-15 19:06:06 +01:00
a4058df9c8 Moved level to separate package (soko).
Moved lineparser to gut package.
2020-01-15 19:03:52 +01:00
a19d33cb9f Changed behaviour of player after brick has been sunken.
- Previously the player would stop on the current tile even if the correct key was pressed. Now the movement continues without interruption.
2020-01-14 19:35:52 +01:00
041cdccc3f Changed behaviour of movement.
- Every movement key at any moment is valid if it is the only movement key pressed (when the previous move is finished).
2020-01-14 19:34:02 +01:00
cdb581d0f6 Added level pack selection. 2020-01-14 19:12:43 +01:00
0203539201 Can load more than a single level pack (but not play yet). 2020-01-14 18:53:15 +01:00
fd92207400 Changed structure of levels/packs. 2020-01-14 18:38:42 +01:00
46e3ac060a Changed Mkdir command to give more rights. 2019-12-31 13:38:10 +01:00
39 changed files with 1489 additions and 471 deletions

View File

@ -14,9 +14,8 @@ type moveAnimation struct {
pos geom.PointF32 pos geom.PointF32
} }
func newMoveAnimation(e *entity, to geom.Point) *moveAnimation { func newMoveAnimation(e *entity, from, to geom.Point) *moveAnimation {
ani := &moveAnimation{e: e, from: e.pos, to: to, pos: e.pos.ToF32()} ani := &moveAnimation{e: e, from: from, to: to, pos: from.ToF32()}
ani.e.pos = to
return ani return ani
} }

View File

@ -3,6 +3,7 @@ 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"
@ -23,9 +24,10 @@ 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 map[string]levelPack Levels *levelPacks
Progress progress Progress progress
SpriteDrawer SpriteDrawer SpriteDrawer SpriteDrawer
Settings settings Settings settings

View File

@ -2,11 +2,12 @@ package main
import ( import (
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
"opslag.de/schobers/krampus19/soko"
) )
type entity struct { type entity struct {
typ entityType id int
pos geom.Point typ soko.EntityType
scr entityLoc scr entityLoc
} }
@ -15,6 +16,6 @@ type entityLoc struct {
z float32 z float32
} }
func newEntity(typ entityType, pos geom.Point) *entity { func newEntity(e soko.Entity, pos geom.Point) *entity {
return &entity{typ, pos, entityLoc{pos.ToF32(), 0}} return &entity{e.ID, e.Typ, entityLoc{pos.ToF32(), 0}}
} }

View File

@ -1,35 +0,0 @@
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:]...)
}

View File

@ -0,0 +1,25 @@
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
}

View File

@ -6,7 +6,11 @@ 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"
@ -98,24 +102,57 @@ func (g *game) loadTextures(pathToName map[string]string) error {
return nil return nil
} }
func (g *game) loadLevelPack(ids ...string) error { func (g *game) loadLevelPacks() error {
g.ctx.Levels = map[string]levelPack{} g.ctx.Levels = newLevelPacks()
for _, id := range ids {
fileName := fmt.Sprintf("levels/pack%s.txt", id) var ids []string
f, err := g.ctx.Resources.Open(fileName) const root = "levels"
err := afero.Walk(g.ctx.ResourcesFs, root, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
defer f.Close() if !info.IsDir() {
pack, err := parseLevelPackAsset(f, func(levelID string) (io.ReadCloser, error) { return nil
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 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 {
err := g.loadLevelPack(id)
if err != nil {
return err
}
}
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 return nil
} }
@ -164,12 +201,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 levels...") log.Println("Loading level packs...")
err = g.loadLevelPack("1") err = g.loadLevelPacks()
if err != nil { if err != nil {
return err return err
} }
log.Printf("Loaded %d levels.\n", len(g.ctx.Levels)) log.Printf("Loaded %d levels packs (%d levels).\n", g.ctx.Levels.Len(), g.ctx.Levels.LenLevels())
log.Println("Loading fonts...") log.Println("Loading fonts...")
err = g.loadFonts() err = g.loadFonts()
@ -192,9 +229,9 @@ func (g *game) Destroy() {
g.ctx.Destroy() g.ctx.Destroy()
} }
func (g *game) Init(disp *allg5.Display, settings settings, res vfs.CopyDir, cons *gut.Console, fps *gut.FPS) error { func (g *game) Init(disp *allg5.Display, settings settings, res vfs.CopyDir, resFs afero.Fs, cons *gut.Console, fps *gut.FPS) error {
log.Print("Initializing game...") log.Print("Initializing game...")
g.ctx = &Context{Resources: res, Textures: map[string]texture{}, Settings: settings, Navigation: navigation{game: g}} g.ctx = &Context{Resources: res, ResourcesFs: resFs, 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()

View File

@ -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, 0600) err = os.MkdirAll(dir, 0777)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -24,10 +24,15 @@ func main() {
} }
} }
func resources() (vfs.CopyDir, error) { func resources() (vfs.CopyDir, afero.Fs, 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")
return vfs.NewCopyDir(vfs.NewFallbackFs(osFs, embeddedFs)) var fs = 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 {
@ -35,7 +40,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, err := resources() res, resFs, err := resources()
if err != nil { if err != nil {
return err return err
} }
@ -107,7 +112,7 @@ func run() error {
defer fps.Destroy() defer fps.Destroy()
game := &game{} game := &game{}
err = game.Init(disp, settings, res, cons, fps) err = game.Init(disp, settings, res, resFs, cons, fps)
if err != nil { if err != nil {
return err return err
} }

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" "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]level levels map[string]soko.Level
} }
func (p levelPack) find(level string) int { func (p levelPack) find(level string) int {
@ -29,6 +33,8 @@ 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
@ -36,19 +42,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 := parseLines(r, ctx.Parse) err := gut.ParseLines(r, ctx.Parse)
if err != nil { if err != nil {
return levelPack{}, err 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 { 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 := parseLevelAsset(rc) level, err := soko.ParseLevel(rc)
if err != nil { if err != nil {
return levelPack{}, err return levelPack{}, err
} }
@ -59,65 +65,65 @@ func parseLevelPackAsset(r io.Reader, openLevelFn func(id string) (io.ReadCloser
return pack, nil return pack, nil
} }
func (c *parseLevelPackContext) Parse(p *lineParser) parseLineFn { func (c *parseLevelPackContext) Parse(p *gut.LineParser) gut.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 *lineParser) parseLineFn { func (c *parseLevelPackContext) parse(p *gut.LineParser) gut.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 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 levelTag = "level:"
const levelsEndTag = ":levels" const levelsEndTag = ":levels"
line := p.next() line := p.Next()
switch line { switch line {
case levelTag: case levelTag:
return skipSpaceBeforeContent(c.parseLevel) return gut.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 *lineParser) parseLineFn { func (c *parseLevelPackContext) parseLevel(p *gut.LineParser) gut.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 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" const levelEndTag = ":level"
line := p.next() line := p.Next()
switch { switch {
case strings.HasPrefix(line, levelEndTag): 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

@ -0,0 +1,36 @@
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 }

View File

@ -0,0 +1,40 @@
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() {}

View File

@ -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[s.packID] s.pack = s.ctx.Levels.ByID(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,12 +27,20 @@ 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
} }

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

@ -11,11 +11,15 @@ 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(alui.NewMargins(&alui.Label{ControlBase: alui.ControlBase{Font: "header"}, Text: "Sokodragon", TextAlign: allg5.AlignCenter}, 3*margin)) m.AddChild(newHeader("Sokodragon"))
m.Add("Play", func() { m.ctx.Navigation.SelectLevel("1") }) m.Add("Play", func() { m.ctx.Navigation.SelectPack() })
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

View File

@ -28,6 +28,15 @@ 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{})
} }

View File

@ -3,11 +3,12 @@ package main
import ( import (
"fmt" "fmt"
"log" "log"
"sort" "strconv"
"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 {
@ -25,21 +26,24 @@ 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) CountPressed(keys ...allg5.Key) int { func (s keyPressedState) ArePressed(keys ...allg5.Key) []allg5.Key {
var cnt int pressed := make([]allg5.Key, 0, len(keys))
for _, k := range keys { for _, k := range keys {
if s[k] { if s[k] {
cnt++ pressed = append(pressed, k)
} }
} }
return cnt return pressed
} }
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
@ -82,6 +86,9 @@ 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() })
@ -116,13 +123,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 == tileNothing || tile == tileInvalid { if tile == soko.TileTypeNothing || tile == soko.TileTypeInvalid {
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)
@ -161,15 +168,10 @@ 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 l.ctx.Settings.Controls.MoveUp: case allg5.KeyF5:
l.state.TryPlayerMove(geom.Pt(0, -1), e.KeyCode) l.pathFinding = !l.pathFinding
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)
} }
} }
} }
@ -193,19 +195,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 tileBasic: case soko.TileTypeBasic:
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 tileMagma: case soko.TileTypeMagma:
l.drawSprite("magma", "magma", scr) l.drawSprite("magma", "magma", scr)
brick := l.state.FindSunkenBrick(pos) brick := l.state.FindSunkenBrickEntity(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)
@ -216,26 +218,30 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
} }
} }
entities := l.state.Entities() entities := l.state.Entities().RenderOrder()
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 entityTypeBrick: case soko.EntityTypeBrick:
l.drawSprite("brick", "brick", e.scr) l.drawSprite("brick", "brick", e.scr)
case entityTypeCharacter: case soko.EntityTypePlayer:
l.drawSprite("dragon", "dragon", e.scr) l.drawSprite("dragon", "dragon", e.scr)
case entityTypeEgg: case soko.EntityTypeTarget:
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)

View File

@ -7,17 +7,19 @@ 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 level level soko.Level
state soko.State
player *entity player *entity
egg *entity egg *entity
bricks entityList bricks entityMap
sunken entityList sunken entityMap
splash map[geom.Point]*splashAnimation splash map[geom.Point]*splashAnimation
steps int steps int
@ -29,9 +31,14 @@ type playLevelState struct {
keysDown keyPressedState keysDown keyPressedState
} }
func (s *playLevelState) Entities() entityList { func (s *playLevelState) Entities() entityMap {
var entities entityList entities := entityMap{}
return entities.Add(s.player).Add(s.egg).AddList(s.bricks) entities[s.player.id] = s.player
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 {
@ -48,39 +55,48 @@ func (s *playLevelState) Particles(at geom.Point) []particle {
return particles return particles
} }
func (s *playLevelState) FindSunkenBrick(pos geom.Point) *entity { func (s *playLevelState) FindSunkenBrickEntity(pos geom.Point) *entity {
return s.sunken.FindEntity(pos) idx := s.level.PosToIdx(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.checkTile(pos.Add2D(1, 0), s.isMagma) || return s.state.Any(soko.IsMagma, soko.Neighbours(pos)...)
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[pack] s.pack = ctx.Levels.ByID(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 {
switch e { newEntity := func(e soko.Entity) *entity { return newEntity(e, s.state.IdxToPos[e.Pos]) }
case entityTypeBrick:
s.bricks = append(s.bricks, newEntity(e, s.level.idxToPos(i))) s.player = newEntity(s.state.Player)
case entityTypeCharacter: s.egg = newEntity(s.state.Target)
s.player = newEntity(e, s.level.idxToPos(i)) s.bricks = entityMap{}
case entityTypeEgg: for _, e := range s.state.Entities {
s.egg = newEntity(e, s.level.idxToPos(i)) if e.Typ != soko.EntityTypeBrick {
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() level { return s.level } func (s *playLevelState) Level() soko.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
@ -96,114 +112,77 @@ func (s *playLevelState) Tick(now time.Duration) {
s.ani.Animate(now) s.ani.Animate(now)
} }
func (s *playLevelState) TryPlayerMove(dir geom.Point, key allg5.Key) { func (s *playLevelState) TryPlayerMove(key allg5.Key) {
if s.player.scr.pos != s.player.pos.ToF32() { var dir soko.Direction
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
} }
to := s.player.pos.Add(dir) state, ok := s.state.MovePlayer(dir)
if !s.canMove(s.player.pos, dir) { if !ok {
log.Printf("Move is not allowed (tried out move to %s after key '%s' was pressed)", to, gut.KeyToString(key)) log.Printf("Move is not allowed (tried out move %s)", dir.String())
return return
} }
s.steps++ to := state.Player.Pos
log.Printf("Moving player to %s", to) toPt := state.IdxToPos[to]
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, to), func() {
log.Printf("Player movement finished")
if s.player.pos == s.egg.pos {
s.complete = true
if onComplete := s.onComplete; onComplete != nil {
onComplete()
}
} else if s.keysDown[key] && s.keysDown.CountPressed(s.ctx.Settings.Controls.MovementKeys()...) == 1 {
log.Printf("Key %s is still down, moving further", gut.KeyToString(key))
s.TryPlayerMove(dir, key)
}
})
if brick := s.bricks.FindEntity(to); brick != nil { if brickID := s.state.Bricks[to]; brickID != -1 {
log.Printf("Pushing brick at %s", to) log.Printf("Brick %d moved", brickID)
brickTo := to.Add(dir) brick := s.bricks[brickID]
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(brick, brickTo), func() { 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") log.Printf("Brick movement finished")
if s.checkTile(brickTo, s.wouldBrickSink) { if sunkenBrickID := state.SunkenBricks[brickTo]; sunkenBrickID == brickID {
log.Printf("Sinking brick at %s", brickTo) log.Printf("Sinking brick %d", brickID)
s.bricks = s.bricks.Remove(brickTo) delete(s.bricks, brickID)
s.sunken = s.sunken.Add(brick) s.sunken[brickID] = brick
s.ani.Start(s.ctx.Tick, newSinkAnimation(brick)) s.ani.Start(s.ctx.Tick, newSinkAnimation(brick))
splash := newSplashAnimation(brickTo) splash := newSplashAnimation(brickToPt)
s.splash[brickTo] = splash s.splash[brickToPt] = splash
s.ani.StartFn(s.ctx.Tick, splash, func() { s.ani.StartFn(s.ctx.Tick, splash, func() {
delete(s.splash, brickTo) delete(s.splash, brickToPt)
}) })
} }
}) })
} }
}
func (s *playLevelState) canMove(from, dir geom.Point) bool { s.steps++
to := from.Add(dir) log.Printf("Moving player to %s", toPt)
if !s.checkTile(to, s.isSolidTile) { s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, playerPt, toPt), func() {
return false log.Printf("Player movement finished")
} if to == state.Target.Pos {
brick := s.bricks.FindEntity(to) s.complete = true
if brick != nil { if onComplete := s.onComplete; onComplete != nil {
brickTo := to.Add(dir) onComplete()
return !s.checkTileNotFound(brickTo, s.isObstructed, true) }
} } else {
return true pressed := s.keysDown.ArePressed(s.ctx.Settings.Controls.MovementKeys()...)
} if len(pressed) == 1 {
key := pressed[0]
func (s *playLevelState) checkTile(pos geom.Point, check func(pos geom.Point, idx int, t tile) bool) bool { log.Printf("Movement key %s is down, moving further", gut.KeyToString(key))
return s.checkTileNotFound(pos, check, false) s.TryPlayerMove(key)
} }
}
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) s.state = state
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
} }

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
"opslag.de/schobers/krampus19/gut"
) )
type sprite struct { type sprite struct {
@ -33,7 +34,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 := parseLines(r, ctx.parse) err := gut.ParseLines(r, ctx.parse)
if err != nil { if err != nil {
return sprite{}, err return sprite{}, err
} }
@ -45,44 +46,44 @@ type spriteContext struct {
part *spritePart part *spritePart
} }
func (c *spriteContext) parse(p *lineParser) parseLineFn { func (c *spriteContext) parse(p *gut.LineParser) gut.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 *lineParser) parseLineFn { func (c *spriteContext) parseContent(p *gut.LineParser) gut.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(errUnexpectedEnd) return p.EmitErr(gut.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 *lineParser) parseLineFn { func (c *spriteContext) parsePart(p *gut.LineParser) gut.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
@ -110,14 +111,14 @@ func (c *spriteContext) parsePart(p *lineParser) 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(errUnexpectedEnd) return p.EmitErr(gut.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):])
@ -125,7 +126,7 @@ func (c *spriteContext) parsePart(p *lineParser) 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):])
@ -133,16 +134,16 @@ func (c *spriteContext) parsePart(p *lineParser) 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"))
} }

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
}

64
soko/direction.go Normal file
View File

@ -0,0 +1,64 @@
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
}

48
soko/entity.go Normal file
View File

@ -0,0 +1,48 @@
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
}

31
soko/entitylocations.go Normal file
View File

@ -0,0 +1,31 @@
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) }

23
soko/entitytype.go Normal file
View File

@ -0,0 +1,23 @@
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
}

91
soko/level.go Normal file
View File

@ -0,0 +1,91 @@
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)
}

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 []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
}

17
soko/locations.go Normal file
View File

@ -0,0 +1,17 @@
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) }

6
soko/moves.go Normal file
View File

@ -0,0 +1,6 @@
package soko
type Moves struct {
All [4]int
Valid []int
}

133
soko/pathfinder.go Normal file
View File

@ -0,0 +1,133 @@
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)
}
}

94
soko/solver.go Normal file
View File

@ -0,0 +1,94 @@
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
}
}
}

111
soko/solver_test.go Normal file
View File

@ -0,0 +1,111 @@
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 Normal file
View File

@ -0,0 +1,240 @@
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
}

71
soko/statecost.go Normal file
View File

@ -0,0 +1,71 @@
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)
}

21
soko/tiletype.go Normal file
View File

@ -0,0 +1,21 @@
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
}