Added solver & optimized state.

Positions are kept by index only.
This commit is contained in:
Sander Schobers 2020-01-16 07:33:04 +01:00
parent 8fc459e1d6
commit 365e9dbbbb
12 changed files with 660 additions and 149 deletions

View File

@ -16,6 +16,6 @@ type entityLoc struct {
z float32 z float32
} }
func newEntity(e soko.Entity) *entity { func newEntity(e soko.Entity, pos geom.Point) *entity {
return &entity{e.ID, e.Typ, entityLoc{e.Pos.ToF32(), 0}} return &entity{e.ID, e.Typ, entityLoc{pos.ToF32(), 0}}
} }

View File

@ -233,12 +233,12 @@ func (l *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) {
font := ctx.Fonts.Get("default") font := ctx.Fonts.Get("default")
if l.pathFinding { if l.pathFinding {
dists := soko.NewPathFinder(l.state.state).FindDistances() dists := soko.NewPathFinder(&l.state.state).FindDistances()
for i := range level.Tiles { for i := 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}
posDist := l.posToScreenF32(scr.pos, -20) 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]))
} }
} }

View File

@ -56,8 +56,12 @@ func (s *playLevelState) Particles(at geom.Point) []particle {
} }
func (s *playLevelState) FindSunkenBrickEntity(pos geom.Point) *entity { func (s *playLevelState) FindSunkenBrickEntity(pos geom.Point) *entity {
id, ok := s.state.SunkenBricks[pos] idx := s.level.PosToIdx(pos)
if ok { if idx == -1 {
return nil
}
id := s.state.SunkenBricks[idx]
if id != -1 {
return s.sunken[id] return s.sunken[id]
} }
return nil return nil
@ -76,11 +80,16 @@ func (s *playLevelState) Init(ctx *Context, pack, level string, onComplete func(
s.sunken = nil s.sunken = nil
s.splash = map[geom.Point]*splashAnimation{} 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.player = newEntity(s.state.Player)
s.egg = newEntity(s.state.Target) s.egg = newEntity(s.state.Target)
s.bricks = entityMap{} s.bricks = entityMap{}
for pos, id := range s.state.Bricks { for _, e := range s.state.Entities {
s.bricks[id] = newEntity(soko.Entity{ID: id, Pos: pos, Typ: soko.EntityTypeBrick}) 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.sunken = entityMap{}
s.keysDown = keyPressedState{} s.keysDown = keyPressedState{}
@ -121,7 +130,8 @@ func (s *playLevelState) TryPlayerMove(key allg5.Key) {
} }
func (s *playLevelState) tryPlayerMove(dir soko.Direction) { 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 return
} }
@ -132,31 +142,33 @@ func (s *playLevelState) tryPlayerMove(dir soko.Direction) {
} }
to := state.Player.Pos 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) log.Printf("Brick %d moved", brickID)
brick := s.bricks[brickID] brick := s.bricks[brickID]
brickTo := state.Entities[brickID].Pos brickTo := state.Entities.ByID(brickID).Pos
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(brick, to, brickTo), func() { 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 sunkenBrickID, ok := state.SunkenBricks[brickTo]; ok && sunkenBrickID == brickID { if sunkenBrickID := state.SunkenBricks[brickTo]; sunkenBrickID == brickID {
log.Printf("Sinking brick %d", brickID) log.Printf("Sinking brick %d", brickID)
delete(s.bricks, brickID) delete(s.bricks, brickID)
s.sunken[brickID] = 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)
}) })
} }
}) })
} }
s.steps++ s.steps++
log.Printf("Moving player to %s", to) log.Printf("Moving player to %s", toPt)
s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, s.state.Player.Pos, to), func() { s.ani.StartFn(s.ctx.Tick, newMoveAnimation(s.player, playerPt, toPt), func() {
log.Printf("Player movement finished") log.Printf("Player movement finished")
if to == state.Target.Pos { if to == state.Target.Pos {
s.complete = true s.complete = true

View File

@ -13,6 +13,20 @@ const (
var Directions = [4]Direction{DirectionUp, 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 { func (d Direction) String() string {
switch d { switch d {
case DirectionUp: case DirectionUp:

View File

@ -1,19 +1,48 @@
package soko package soko
import "opslag.de/schobers/geom"
type Entity struct { type Entity struct {
ID int ID int
Pos geom.Point Pos int
Typ EntityType 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 { func (e Entities) Clone() Entities {
clone := Entities{} clone := make(Entities, len(e))
for id, e := range e { copy(clone, e)
clone[id] = 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 return clone
} }

View File

@ -5,8 +5,9 @@ import (
) )
type Level struct { type Level struct {
Width int Width int
Height int Height int
Tiles []TileType Tiles []TileType
Entities []EntityType Entities []EntityType
} }
@ -20,28 +21,71 @@ func (l Level) PosToIdx(p geom.Point) int {
return p.Y*l.Width + p.X return p.Y*l.Width + p.X
} }
func (l Level) State() State { func (l Level) Moves() []Moves {
s := NewState() moves := make([]Moves, len(l.Tiles))
for i, t := range l.Tiles { w := l.Width
pos := l.IdxToPos(i) for y := 0; y < l.Height; y++ {
switch t { for x := 0; x < w; x++ {
case TileTypeBasic: var m Moves
s.BasicTiles.Add(pos) idx := y*w + x
case TileTypeMagma: if y > 0 {
s.MagmaTiles.Add(pos) 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
for i, e := range l.Entities { }
pos := l.IdxToPos(i)
switch e { func (l Level) MoveIdx(idx int, dir Direction) int {
case EntityTypeBrick: switch dir {
s.AddBrick(pos) case DirectionUp:
case EntityTypePlayer: if idx < l.Width {
s.SetPlayer(pos) return -1
case EntityTypeTarget: }
s.SetTarget(pos) return idx - l.Width
} case DirectionRight:
} if (idx+1)%l.Width == 0 {
return s 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
View File

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

View File

@ -2,69 +2,57 @@ package soko
import ( import (
"sort" "sort"
"opslag.de/schobers/geom"
) )
type PathFinder struct { type PathFinder struct {
state State state *State
moves []Moves
} }
func NewPathFinder(s State) PathFinder { func NewPathFinder(s *State) PathFinder {
return PathFinder{s} return PathFinder{s, nil}
} }
type betterNeighbourFn func(geom.Point, int) func NewPathFinderMoves(s *State, moves []Moves) PathFinder {
return PathFinder{s, moves}
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 (p PathFinder) Find(target geom.Point) []geom.Point { func (p PathFinder) Find(target int) []int {
source := p.state.Player.Pos source := p.state.Player.Pos
frontier := []geom.Point{source} level := p.state.Level
distances := map[geom.Point]int{source: 0} size := level.Size()
moves := map[geom.Point]geom.Point{source: source} distances := NewInts(size)
distances[source] = 0
moves := NewInts(size)
moves[source] = source
state := p.newPathFinderState(source)
heuristic := func(i int) int { heuristic := func(i int) int {
pos := frontier[i] idx := state.frontier[i]
return distances[pos] + pos.DistInt(target) return distances[idx] + p.state.IdxToPos[idx].DistInt(p.state.IdxToPos[target])
} }
for { for {
curr := frontier[0] curr := state.frontier[0]
if curr == target { if curr == target {
break break
} }
frontier = frontier[1:] state.frontier = state.frontier[1:]
p.findBetterNeighbours(distances, curr, func(next geom.Point, newDistance int) { state.findBetterNeighbours(distances, curr, func(nextIdx, newDistance int) {
moves[next] = curr moves[nextIdx] = curr
frontier = append(frontier, next)
}) })
if len(frontier) == 0 { if len(state.frontier) == 0 {
return nil // no path return nil // no path
} }
// apply heuristic to frontier (favor points closer to target) // 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 // build reverse path
curr := target curr := target
path := []geom.Point{curr} path := []int{curr}
for { for {
curr = moves[curr] curr = moves[curr]
if curr == source { if curr == source {
@ -80,20 +68,66 @@ func (p PathFinder) Find(target geom.Point) []geom.Point {
return path return path
} }
func (p PathFinder) FindDistances() map[geom.Point]int { func (p PathFinder) FindDistances() Ints {
source := p.state.Player.Pos source := p.state.Player.Pos
frontier := []geom.Point{source} level := p.state.Level
distances := map[geom.Point]int{source: 0} size := level.Size()
distances := NewInts(size)
distances[source] = 0
state := p.newPathFinderState(source)
for { for {
curr := frontier[0] curr := state.frontier[0]
frontier = frontier[1:] state.frontier = state.frontier[1:]
p.findBetterNeighbours(distances, curr, func(next geom.Point, newDistance int) { state.findBetterNeighbours(distances, curr, nil)
frontier = append(frontier, next)
})
if len(frontier) == 0 { if len(state.frontier) == 0 {
return distances 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)
}
}

View File

@ -6,25 +6,48 @@ type State struct {
Player Entity Player Entity
Target Entity Target Entity
BasicTiles Locations
MagmaTiles Locations
Bricks EntityLocations
SunkenBricks EntityLocations
Entities Entities Entities Entities
Level Level
IdxToPos []geom.Point
Bricks Ints
SunkenBricks Ints
Walkable []bool
nextEntityID int nextEntityID int
} }
func NewState() State { func NewState(l Level) State {
return State{ size := l.Size()
BasicTiles: Locations{}, s := State{
MagmaTiles: Locations{},
Bricks: EntityLocations{},
SunkenBricks: EntityLocations{},
Entities: Entities{}, 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. // Clone creates and returns a clone of the state.
@ -32,21 +55,16 @@ func (s State) Clone() State {
return State{ return State{
Player: s.Player, Player: s.Player,
Target: s.Target, Target: s.Target,
BasicTiles: s.BasicTiles.Clone(), Entities: s.Entities.Clone(),
MagmaTiles: s.MagmaTiles.Clone(), Level: s.Level,
IdxToPos: s.IdxToPos,
Bricks: s.Bricks.Clone(), Bricks: s.Bricks.Clone(),
SunkenBricks: s.SunkenBricks.Clone(), SunkenBricks: s.SunkenBricks.Clone(),
Entities: s.Entities.Clone(), Walkable: append(s.Walkable[:0:0], s.Walkable...),
nextEntityID: s.nextEntityID, 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 { func (s State) All(pred func(State, geom.Point) bool, p ...geom.Point) bool {
for _, p := range p { for _, p := range p {
if !pred(s, 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 return false
} }
func (s State) IsOpenForBrick(p geom.Point) bool { func (s *State) Equal(other *State) bool {
if s.IsWalkable(p) { for i := 0; i < len(s.Entities); i++ {
return !s.Bricks.Has(p) 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 { 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. // 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) { func (s State) MovePlayer(dir Direction) (State, bool) {
to := s.Player.Pos.Add(dir.ToPoint()) to := s.Level.MoveIdx(s.Player.Pos, dir)
if to == -1 {
if !s.IsWalkable(to) {
return s, false 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) { return s.Mutate(func(s *State) {
s.movePlayer(to) s.movePlayer(to)
}), true }), true
} }
brickTo := to.Add(dir.ToPoint()) brickTo := s.Level.MoveIdx(to, dir)
if !s.IsOpenForBrick(brickTo) { if brickTo == -1 {
return s, false
}
if !s.isOpenForBrick(brickTo) {
return s, false return s, false
} }
brickID := s.Bricks[to]
return s.Mutate(func(s *State) { return s.Mutate(func(s *State) {
s.movePlayer(to) s.movePlayer(to)
if s.MagmaTiles[brickTo] && !s.SunkenBricks.Has(brickTo) { if s.Level.Tiles[brickTo] == TileTypeMagma && s.SunkenBricks[brickTo] == -1 {
s.Bricks.Remove(to) s.SunkenBricks[brickTo] = brickID
s.SunkenBricks.Add(brickTo, brickID) s.Walkable[brickTo] = true
s.Bricks[to] = -1
} else { } 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 }), 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. // Mutate clones the state, applies the mutation on the clone and returns the clone.
func (s State) Mutate(fn func(s *State)) State { func (s State) Mutate(fn func(s *State)) State {
clone := s.Clone() clone := s.Clone()
@ -116,29 +182,59 @@ func (s State) Mutate(fn func(s *State)) State {
return clone return clone
} }
func (s *State) SetPlayer(pos geom.Point) { func (s *State) addBrick(pos geom.Point) {
s.addEntity(pos, EntityTypePlayer, func(e Entity) { s.addEntity(pos, EntityTypeBrick, func(e Entity) {
s.Player = e idx := s.Level.PosToIdx(pos)
}) if idx != -1 {
} s.Bricks[idx] = e.ID
}
func (s *State) SetTarget(pos geom.Point) {
s.addEntity(pos, EntityTypeTarget, func(e Entity) {
s.Target = e
}) })
} }
func (s *State) addEntity(pos geom.Point, typ EntityType, add func(Entity)) { func (s *State) addEntity(pos geom.Point, typ EntityType, add func(Entity)) {
id := s.nextEntityID id := s.nextEntityID
e := Entity{id, pos, typ} e := Entity{id, s.Level.PosToIdx(pos), typ}
add(e) add(e)
s.Entities[id] = e s.Entities = append(s.Entities, e)
s.nextEntityID++ s.nextEntityID++
} }
func (s *State) movePlayer(to geom.Point) { func (s *State) initPlayer(pos geom.Point) {
s.Player.Pos = to s.addEntity(pos, EntityTypePlayer, func(e Entity) {
s.Entities[s.Player.ID] = Entity{s.Player.ID, to, EntityTypePlayer} 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
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)
}