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

View File

@ -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]))
}
}

View File

@ -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

View File

@ -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:

View File

@ -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
}

View File

@ -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
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 (
"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
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
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
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)
}