From 365e9dbbbb85b6fbfe3b2a90afb56a852d1a8562 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Thu, 16 Jan 2020 07:33:04 +0100 Subject: [PATCH] Added solver & optimized state. Positions are kept by index only. --- cmd/krampus19/entity.go | 4 +- cmd/krampus19/playlevel.go | 4 +- cmd/krampus19/playlevelstate.go | 40 ++++--- soko/direction.go | 14 +++ soko/entity.go | 45 +++++-- soko/level.go | 92 ++++++++++---- soko/moves.go | 6 + soko/pathfinder.go | 124 ++++++++++++------- soko/solver.go | 94 +++++++++++++++ soko/solver_test.go | 111 +++++++++++++++++ soko/state.go | 204 +++++++++++++++++++++++--------- soko/statecost.go | 71 +++++++++++ 12 files changed, 660 insertions(+), 149 deletions(-) create mode 100644 soko/moves.go create mode 100644 soko/solver.go create mode 100644 soko/solver_test.go create mode 100644 soko/statecost.go diff --git a/cmd/krampus19/entity.go b/cmd/krampus19/entity.go index 8652d5e..09e510a 100644 --- a/cmd/krampus19/entity.go +++ b/cmd/krampus19/entity.go @@ -16,6 +16,6 @@ type entityLoc struct { z float32 } -func newEntity(e soko.Entity) *entity { - return &entity{e.ID, e.Typ, entityLoc{e.Pos.ToF32(), 0}} +func newEntity(e soko.Entity, pos geom.Point) *entity { + return &entity{e.ID, e.Typ, entityLoc{pos.ToF32(), 0}} } diff --git a/cmd/krampus19/playlevel.go b/cmd/krampus19/playlevel.go index 78e78d7..7089b30 100644 --- a/cmd/krampus19/playlevel.go +++ b/cmd/krampus19/playlevel.go @@ -233,12 +233,12 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { font := ctx.Fonts.Get("default") if l.pathFinding { - dists := soko.NewPathFinder(l.state.state).FindDistances() + 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[pos])) + ctx.Fonts.DrawAlignFont(font, posDist.X, posDist.Y, posDist.X, ctx.Palette.Text, allg5.AlignCenter, strconv.Itoa(dists[i])) } } diff --git a/cmd/krampus19/playlevelstate.go b/cmd/krampus19/playlevelstate.go index 0c52f82..9a7d110 100644 --- a/cmd/krampus19/playlevelstate.go +++ b/cmd/krampus19/playlevelstate.go @@ -56,8 +56,12 @@ func (s *playLevelState) Particles(at geom.Point) []particle { } func (s *playLevelState) FindSunkenBrickEntity(pos geom.Point) *entity { - id, ok := s.state.SunkenBricks[pos] - if ok { + idx := s.level.PosToIdx(pos) + if idx == -1 { + return nil + } + id := s.state.SunkenBricks[idx] + if id != -1 { return s.sunken[id] } return nil @@ -76,11 +80,16 @@ func (s *playLevelState) Init(ctx *Context, pack, level string, onComplete func( s.sunken = nil s.splash = map[geom.Point]*splashAnimation{} + newEntity := func(e soko.Entity) *entity { return newEntity(e, s.state.IdxToPos[e.Pos]) } + s.player = newEntity(s.state.Player) s.egg = newEntity(s.state.Target) s.bricks = entityMap{} - for pos, id := range s.state.Bricks { - s.bricks[id] = newEntity(soko.Entity{ID: id, Pos: pos, Typ: soko.EntityTypeBrick}) + for _, e := range s.state.Entities { + 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{} @@ -121,7 +130,8 @@ func (s *playLevelState) TryPlayerMove(key allg5.Key) { } func (s *playLevelState) tryPlayerMove(dir soko.Direction) { - if s.player.scr.pos != s.state.Player.Pos.ToF32() { + playerPt := s.state.IdxToPos[s.state.Player.Pos] + if s.player.scr.pos != playerPt.ToF32() { return } @@ -132,31 +142,33 @@ func (s *playLevelState) tryPlayerMove(dir soko.Direction) { } to := state.Player.Pos + toPt := state.IdxToPos[to] - if brickID, ok := s.state.Bricks[to]; ok { + if brickID := s.state.Bricks[to]; brickID != -1 { log.Printf("Brick %d moved", brickID) brick := s.bricks[brickID] - brickTo := state.Entities[brickID].Pos - s.ani.StartFn(s.ctx.Tick, newMoveAnimation(brick, to, 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") - if sunkenBrickID, ok := state.SunkenBricks[brickTo]; ok && sunkenBrickID == brickID { + 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(brickTo) - s.splash[brickTo] = splash + splash := newSplashAnimation(brickToPt) + s.splash[brickToPt] = splash s.ani.StartFn(s.ctx.Tick, splash, func() { - delete(s.splash, brickTo) + delete(s.splash, brickToPt) }) } }) } s.steps++ - log.Printf("Moving player to %s", to) - s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, s.state.Player.Pos, to), func() { + log.Printf("Moving player to %s", toPt) + s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, playerPt, toPt), func() { log.Printf("Player movement finished") if to == state.Target.Pos { s.complete = true diff --git a/soko/direction.go b/soko/direction.go index 288ae89..c421b91 100644 --- a/soko/direction.go +++ b/soko/direction.go @@ -13,6 +13,20 @@ const ( 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: diff --git a/soko/entity.go b/soko/entity.go index 5b46142..d11befc 100644 --- a/soko/entity.go +++ b/soko/entity.go @@ -1,19 +1,48 @@ package soko -import "opslag.de/schobers/geom" - type Entity struct { ID int - Pos geom.Point + Pos int Typ EntityType } -type Entities map[int]Entity +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 := Entities{} - for id, e := range e { - clone[id] = e - } + 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 } diff --git a/soko/level.go b/soko/level.go index 03d902f..9de96b4 100644 --- a/soko/level.go +++ b/soko/level.go @@ -5,8 +5,9 @@ import ( ) type Level struct { - Width int - Height int + Width int + Height int + Tiles []TileType Entities []EntityType } @@ -20,28 +21,71 @@ func (l Level) PosToIdx(p geom.Point) int { return p.Y*l.Width + p.X } -func (l Level) State() State { - s := NewState() - for i, t := range l.Tiles { - pos := l.IdxToPos(i) - switch t { - case TileTypeBasic: - s.BasicTiles.Add(pos) - case TileTypeMagma: - s.MagmaTiles.Add(pos) +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 } } - - for i, e := range l.Entities { - pos := l.IdxToPos(i) - switch e { - case EntityTypeBrick: - s.AddBrick(pos) - case EntityTypePlayer: - s.SetPlayer(pos) - case EntityTypeTarget: - s.SetTarget(pos) - } - } - return s + 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) } diff --git a/soko/moves.go b/soko/moves.go new file mode 100644 index 0000000..4aa30f3 --- /dev/null +++ b/soko/moves.go @@ -0,0 +1,6 @@ +package soko + +type Moves struct { + All [4]int + Valid []int +} diff --git a/soko/pathfinder.go b/soko/pathfinder.go index c517272..59af71a 100644 --- a/soko/pathfinder.go +++ b/soko/pathfinder.go @@ -2,69 +2,57 @@ package soko import ( "sort" - - "opslag.de/schobers/geom" ) type PathFinder struct { - state State + state *State + moves []Moves } -func NewPathFinder(s State) PathFinder { - return PathFinder{s} +func NewPathFinder(s *State) PathFinder { + return PathFinder{s, nil} } -type betterNeighbourFn func(geom.Point, int) - -func (p PathFinder) findBetterNeighbours(distances map[geom.Point]int, curr geom.Point, better betterNeighbourFn) { - currDistance := distances[curr] - newDistance := currDistance + 1 - - neighbours := Neighbours(curr) - for _, next := range neighbours { - if !p.state.IsWalkable(next) || p.state.Bricks.Has(next) { // filter neighbours - continue - } - if distance, ok := distances[next]; ok && distance <= newDistance { // skip when shorter path exists - continue - } - distances[next] = newDistance - better(next, newDistance) - } +func NewPathFinderMoves(s *State, moves []Moves) PathFinder { + return PathFinder{s, moves} } -func (p PathFinder) Find(target geom.Point) []geom.Point { +func (p PathFinder) Find(target int) []int { source := p.state.Player.Pos - frontier := []geom.Point{source} - distances := map[geom.Point]int{source: 0} - moves := map[geom.Point]geom.Point{source: source} + 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 { - pos := frontier[i] - return distances[pos] + pos.DistInt(target) + idx := state.frontier[i] + return distances[idx] + p.state.IdxToPos[idx].DistInt(p.state.IdxToPos[target]) } for { - curr := frontier[0] + curr := state.frontier[0] if curr == target { break } - frontier = frontier[1:] + state.frontier = state.frontier[1:] - p.findBetterNeighbours(distances, curr, func(next geom.Point, newDistance int) { - moves[next] = curr - frontier = append(frontier, next) + state.findBetterNeighbours(distances, curr, func(nextIdx, newDistance int) { + moves[nextIdx] = curr }) - if len(frontier) == 0 { + if len(state.frontier) == 0 { return nil // no path } // apply heuristic to frontier (favor points closer to target) - sort.Slice(frontier, func(i, j int) bool { return heuristic(i) < heuristic(j) }) + sort.Slice(state.frontier, func(i, j int) bool { return heuristic(i) < heuristic(j) }) } // build reverse path curr := target - path := []geom.Point{curr} + path := []int{curr} for { curr = moves[curr] if curr == source { @@ -80,20 +68,66 @@ func (p PathFinder) Find(target geom.Point) []geom.Point { return path } -func (p PathFinder) FindDistances() map[geom.Point]int { +func (p PathFinder) FindDistances() Ints { source := p.state.Player.Pos - frontier := []geom.Point{source} - distances := map[geom.Point]int{source: 0} + level := p.state.Level + size := level.Size() + distances := NewInts(size) + distances[source] = 0 + state := p.newPathFinderState(source) + for { - curr := frontier[0] - frontier = frontier[1:] + curr := state.frontier[0] + state.frontier = state.frontier[1:] - p.findBetterNeighbours(distances, curr, func(next geom.Point, newDistance int) { - frontier = append(frontier, next) - }) + state.findBetterNeighbours(distances, curr, nil) - if len(frontier) == 0 { + 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) + } +} diff --git a/soko/solver.go b/soko/solver.go new file mode 100644 index 0000000..9ae47af --- /dev/null +++ b/soko/solver.go @@ -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 + } + } +} diff --git a/soko/solver_test.go b/soko/solver_test.go new file mode 100644 index 0000000..7865f29 --- /dev/null +++ b/soko/solver_test.go @@ -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) + } +} diff --git a/soko/state.go b/soko/state.go index 37f6da5..c387b8f 100644 --- a/soko/state.go +++ b/soko/state.go @@ -6,25 +6,48 @@ type State struct { Player Entity Target Entity - BasicTiles Locations - MagmaTiles Locations - - Bricks EntityLocations - SunkenBricks EntityLocations - Entities Entities + Level Level + IdxToPos []geom.Point + Bricks Ints + SunkenBricks Ints + Walkable []bool + nextEntityID int } -func NewState() State { - return State{ - BasicTiles: Locations{}, - MagmaTiles: Locations{}, - Bricks: EntityLocations{}, - SunkenBricks: EntityLocations{}, +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. @@ -32,21 +55,16 @@ func (s State) Clone() State { return State{ Player: s.Player, Target: s.Target, - BasicTiles: s.BasicTiles.Clone(), - MagmaTiles: s.MagmaTiles.Clone(), + Entities: s.Entities.Clone(), + Level: s.Level, + IdxToPos: s.IdxToPos, Bricks: s.Bricks.Clone(), SunkenBricks: s.SunkenBricks.Clone(), - Entities: s.Entities.Clone(), + Walkable: append(s.Walkable[:0:0], s.Walkable...), nextEntityID: s.nextEntityID, } } -func (s *State) AddBrick(pos geom.Point) { - s.addEntity(pos, EntityTypeBrick, func(e Entity) { - s.Bricks.Add(pos, e.ID) - }) -} - func (s State) All(pred func(State, geom.Point) bool, p ...geom.Point) bool { for _, p := range p { if !pred(s, p) { @@ -65,50 +83,98 @@ func (s State) Any(pred func(State, geom.Point) bool, p ...geom.Point) bool { return false } -func (s State) IsOpenForBrick(p geom.Point) bool { - if s.IsWalkable(p) { - return !s.Bricks.Has(p) +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 s.MagmaTiles[p] + return true } -// IsWalkable indicates that a player could walk over this tile regardless of any bricks that might be there. +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 { - return s.BasicTiles[p] || (s.MagmaTiles[p] && s.SunkenBricks.Has(p)) + 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.Player.Pos.Add(dir.ToPoint()) - - if !s.IsWalkable(to) { + to := s.Level.MoveIdx(s.Player.Pos, dir) + if to == -1 { return s, false } - if !s.Bricks.Has(to) { + 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 := to.Add(dir.ToPoint()) - if !s.IsOpenForBrick(brickTo) { + brickTo := s.Level.MoveIdx(to, dir) + if brickTo == -1 { + return s, false + } + if !s.isOpenForBrick(brickTo) { return s, false } - brickID := s.Bricks[to] return s.Mutate(func(s *State) { s.movePlayer(to) - if s.MagmaTiles[brickTo] && !s.SunkenBricks.Has(brickTo) { - s.Bricks.Remove(to) - s.SunkenBricks.Add(brickTo, brickID) + 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.Move(to, brickTo) + s.Bricks[brickTo] = brickID + s.Bricks[to] = -1 } - s.Entities[brickID] = Entity{brickID, brickTo, EntityTypeBrick} + 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() @@ -116,29 +182,59 @@ func (s State) Mutate(fn func(s *State)) State { return clone } -func (s *State) SetPlayer(pos geom.Point) { - s.addEntity(pos, EntityTypePlayer, func(e Entity) { - s.Player = e - }) -} - -func (s *State) SetTarget(pos geom.Point) { - s.addEntity(pos, EntityTypeTarget, func(e Entity) { - s.Target = e +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, pos, typ} + e := Entity{id, s.Level.PosToIdx(pos), typ} add(e) - s.Entities[id] = e + s.Entities = append(s.Entities, e) s.nextEntityID++ } -func (s *State) movePlayer(to geom.Point) { - s.Player.Pos = to - s.Entities[s.Player.ID] = Entity{s.Player.ID, to, EntityTypePlayer} +func (s *State) initPlayer(pos geom.Point) { + s.addEntity(pos, EntityTypePlayer, func(e Entity) { + s.Player = e + }) } -func IsMagma(s State, p geom.Point) bool { return s.MagmaTiles[p] } +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 +} diff --git a/soko/statecost.go b/soko/statecost.go new file mode 100644 index 0000000..8a3b1ea --- /dev/null +++ b/soko/statecost.go @@ -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) +}