diff --git a/cmd/krampus19/playlevel.go b/cmd/krampus19/playlevel.go index 7b21a21..78e78d7 100644 --- a/cmd/krampus19/playlevel.go +++ b/cmd/krampus19/playlevel.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "strconv" "opslag.de/schobers/allg5" "opslag.de/schobers/geom" @@ -25,7 +26,8 @@ type playLevel struct { offset geom.PointF32 scale float32 - state playLevelState + state playLevelState + pathFinding bool } type keyPressedState map[allg5.Key]bool @@ -166,6 +168,8 @@ func (l *playLevel) Handle(e allg5.Event) { case allg5.KeyEscape: l.showMenu = true l.menu.Activate(0) + case allg5.KeyF5: + l.pathFinding = !l.pathFinding } l.state.TryPlayerMove(e.KeyCode) } @@ -227,6 +231,17 @@ 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() + 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])) + } + } + steps := fmt.Sprintf("STEPS: %d", l.state.Steps()) ctx.Fonts.DrawAlignFont(font, bounds.Min.X, 24, bounds.Max.X, ctx.Palette.Text, allg5.AlignCenter, steps) diff --git a/soko/pathfinder.go b/soko/pathfinder.go new file mode 100644 index 0000000..c517272 --- /dev/null +++ b/soko/pathfinder.go @@ -0,0 +1,99 @@ +package soko + +import ( + "sort" + + "opslag.de/schobers/geom" +) + +type PathFinder struct { + state State +} + +func NewPathFinder(s State) PathFinder { + return PathFinder{s} +} + +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 (p PathFinder) Find(target geom.Point) []geom.Point { + source := p.state.Player.Pos + frontier := []geom.Point{source} + distances := map[geom.Point]int{source: 0} + moves := map[geom.Point]geom.Point{source: source} + heuristic := func(i int) int { + pos := frontier[i] + return distances[pos] + pos.DistInt(target) + } + for { + curr := frontier[0] + if curr == target { + break + } + frontier = frontier[1:] + + p.findBetterNeighbours(distances, curr, func(next geom.Point, newDistance int) { + moves[next] = curr + frontier = append(frontier, next) + }) + + if len(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) }) + } + + // build reverse path + curr := target + path := []geom.Point{curr} + for { + curr = moves[curr] + if curr == source { + break + } + path = append(path, curr) + } + // reverse path + n := len(path) + for i := 0; i < n/2; i++ { + path[i], path[n-i-1] = path[n-i-1], path[i] + } + return path +} + +func (p PathFinder) FindDistances() map[geom.Point]int { + source := p.state.Player.Pos + frontier := []geom.Point{source} + distances := map[geom.Point]int{source: 0} + for { + curr := frontier[0] + frontier = frontier[1:] + + p.findBetterNeighbours(distances, curr, func(next geom.Point, newDistance int) { + frontier = append(frontier, next) + }) + + if len(frontier) == 0 { + return distances + } + } +} diff --git a/soko/state.go b/soko/state.go index a9015a1..37f6da5 100644 --- a/soko/state.go +++ b/soko/state.go @@ -41,14 +41,6 @@ func (s State) Clone() State { } } -func (s *State) addEntity(pos geom.Point, typ EntityType, add func(Entity)) { - id := s.nextEntityID - e := Entity{id, pos, typ} - add(e) - s.Entities[id] = e - s.nextEntityID++ -} - func (s *State) AddBrick(pos geom.Point) { s.addEntity(pos, EntityTypeBrick, func(e Entity) { s.Bricks.Add(pos, e.ID) @@ -124,11 +116,6 @@ func (s State) Mutate(fn func(s *State)) State { return clone } -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) SetPlayer(pos geom.Point) { s.addEntity(pos, EntityTypePlayer, func(e Entity) { s.Player = e @@ -141,4 +128,17 @@ func (s *State) SetTarget(pos geom.Point) { }) } +func (s *State) addEntity(pos geom.Point, typ EntityType, add func(Entity)) { + id := s.nextEntityID + e := Entity{id, pos, typ} + add(e) + s.Entities[id] = 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 IsMagma(s State, p geom.Point) bool { return s.MagmaTiles[p] }