Added solver & optimized state.
Positions are kept by index only.
This commit is contained in:
parent
8fc459e1d6
commit
365e9dbbbb
@ -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}}
|
||||
}
|
||||
|
@ -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]))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
6
soko/moves.go
Normal file
6
soko/moves.go
Normal file
@ -0,0 +1,6 @@
|
||||
package soko
|
||||
|
||||
type Moves struct {
|
||||
All [4]int
|
||||
Valid []int
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
94
soko/solver.go
Normal file
94
soko/solver.go
Normal 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
111
soko/solver_test.go
Normal 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)
|
||||
}
|
||||
}
|
204
soko/state.go
204
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
|
||||
}
|
||||
|
71
soko/statecost.go
Normal file
71
soko/statecost.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user