solver
This commit is contained in:
parent
41f26363c8
commit
e9faf31ac9
10 changed files with 326 additions and 64 deletions
85
cli/game.go
Normal file
85
cli/game.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"15/lib"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"atomicgo.dev/keyboard"
|
||||
"atomicgo.dev/keyboard/keys"
|
||||
)
|
||||
|
||||
type CliGame struct {
|
||||
board *lib.Board
|
||||
}
|
||||
|
||||
func (g *CliGame) PrintState() {
|
||||
// Works only on Linux
|
||||
fmt.Print("\033[H\033[2J")
|
||||
fmt.Printf("To quit game press ESC.\n")
|
||||
g.board.Print()
|
||||
fmt.Printf("Solved: %t\n", g.board.SolvedFast())
|
||||
}
|
||||
|
||||
func (g *CliGame) Loop() {
|
||||
g.PrintState()
|
||||
|
||||
keyboard.Listen(func(key keys.Key) (stop bool, err error) {
|
||||
switch key.Code {
|
||||
case keys.CtrlC, keys.Escape:
|
||||
return true, nil
|
||||
case keys.Up:
|
||||
g.board.Move(lib.DOWN)
|
||||
case keys.Down:
|
||||
g.board.Move(lib.UP)
|
||||
case keys.Left:
|
||||
g.board.Move(lib.RIGHT)
|
||||
case keys.Right:
|
||||
g.board.Move(lib.LEFT)
|
||||
default:
|
||||
fmt.Printf("\rYou pressed: %s\n", key)
|
||||
}
|
||||
|
||||
g.PrintState()
|
||||
|
||||
if g.board.SolvedFast() {
|
||||
fmt.Printf("\rYou Won!\n")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (g *CliGame) Solve() {
|
||||
|
||||
path, cost := lib.Solver(g.board)
|
||||
|
||||
reverse := []*lib.Board{}
|
||||
path.ForEach(func(b *lib.Board) {
|
||||
reverse = append([]*lib.Board{b}, reverse...)
|
||||
})
|
||||
|
||||
for _, b := range reverse {
|
||||
fmt.Print("\033[H\033[2J")
|
||||
fmt.Printf("Cost is: %d\n", cost)
|
||||
b.Print()
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func StartGame() {
|
||||
game := CliGame{board: lib.NewBoard()}
|
||||
|
||||
game.board.Shuffle(10)
|
||||
|
||||
game.Loop()
|
||||
}
|
||||
|
||||
func StartSolver() {
|
||||
game := CliGame{board: lib.NewBoard()}
|
||||
|
||||
game.board.Shuffle(20)
|
||||
|
||||
game.Solve()
|
||||
}
|
20
cli/main.go
20
cli/main.go
|
@ -1,9 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"15/lib"
|
||||
)
|
||||
import "os"
|
||||
|
||||
func main() {
|
||||
lib.Start()
|
||||
|
||||
message := "Only 'game' and 'solve' options supported"
|
||||
|
||||
if len(os.Args) == 1 {
|
||||
panic(message)
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "game":
|
||||
StartGame()
|
||||
case "solve":
|
||||
StartSolver()
|
||||
default:
|
||||
panic(message)
|
||||
}
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -11,5 +11,6 @@ require github.com/vcaesar/keycode v0.10.0 // indirect
|
|||
|
||||
require (
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/fotonmoton/algorithms v0.0.0-20220110180800-fcd2f9b99aad // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -12,6 +12,8 @@ github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARu
|
|||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fotonmoton/algorithms v0.0.0-20220110180800-fcd2f9b99aad h1:oe2EOICvLMkebV8ukPBCuKruNTA9HcoQ5OwyyTDxXDs=
|
||||
github.com/fotonmoton/algorithms v0.0.0-20220110180800-fcd2f9b99aad/go.mod h1:SNVorQGYffwEPh6lug8Rgh3AkpxzxWfirrPCIt7KVdg=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
|
|
11
lib/board.go
11
lib/board.go
|
@ -42,7 +42,7 @@ func NewBoard() *Board {
|
|||
// no more moves are needed and we can say that
|
||||
// board is solved.
|
||||
func (board *Board) Solved() bool {
|
||||
return board.neededMoves() == 0
|
||||
return board.NeededMoves() == 0
|
||||
}
|
||||
|
||||
// Faster way to check if board is solved.
|
||||
|
@ -113,6 +113,13 @@ func (b *Board) Move(d Direction) {
|
|||
b.empty[1] = newCol
|
||||
}
|
||||
|
||||
func (b *Board) Copy() *Board {
|
||||
return &Board{
|
||||
grid: b.grid,
|
||||
empty: b.empty,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Board) Shuffle(steps int) []Direction {
|
||||
moves := []Direction{}
|
||||
|
||||
|
@ -148,7 +155,7 @@ func (b *Board) Shuffle(steps int) []Direction {
|
|||
// sum of number of moves each board piece should do
|
||||
// to get to desired position. It ignores real "circular"
|
||||
// moves and calculates moves as if only one piece exists on the board.
|
||||
func (board *Board) neededMoves() int {
|
||||
func (board *Board) NeededMoves() int {
|
||||
neededMoves := 0
|
||||
|
||||
for row := 0; row < 4; row++ {
|
||||
|
|
|
@ -53,6 +53,14 @@ func TestPossibleDirections(t *testing.T) {
|
|||
if isLeftPresent == -1 || isUPresent == -1 {
|
||||
t.Error("UP and LEFT directions should be present")
|
||||
}
|
||||
|
||||
board.Move(LEFT)
|
||||
|
||||
directions = board.PossibleDirections()
|
||||
|
||||
if len(directions) != 3 {
|
||||
t.Error("should be 3 possible directions after one move left from initial state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMove(t *testing.T) {
|
||||
|
|
58
lib/game.go
58
lib/game.go
|
@ -1,58 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"atomicgo.dev/keyboard"
|
||||
"atomicgo.dev/keyboard/keys"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
board *Board
|
||||
}
|
||||
|
||||
func (g *Game) PrintState() {
|
||||
// Works only on Linux
|
||||
fmt.Print("\033[H\033[2J")
|
||||
fmt.Printf("To quit game press ESC.\n")
|
||||
g.board.Print()
|
||||
fmt.Printf("Solved: %t\n", g.board.SolvedFast())
|
||||
}
|
||||
|
||||
func (g *Game) Loop() {
|
||||
g.PrintState()
|
||||
|
||||
keyboard.Listen(func(key keys.Key) (stop bool, err error) {
|
||||
switch key.Code {
|
||||
case keys.CtrlC, keys.Escape:
|
||||
return true, nil
|
||||
case keys.Up:
|
||||
g.board.Move(DOWN)
|
||||
case keys.Down:
|
||||
g.board.Move(UP)
|
||||
case keys.Left:
|
||||
g.board.Move(RIGHT)
|
||||
case keys.Right:
|
||||
g.board.Move(LEFT)
|
||||
default:
|
||||
fmt.Printf("\rYou pressed: %s\n", key)
|
||||
}
|
||||
|
||||
g.PrintState()
|
||||
|
||||
if g.board.SolvedFast() {
|
||||
fmt.Printf("\rYou Won!\n")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
func Start() {
|
||||
game := Game{board: NewBoard()}
|
||||
|
||||
game.board.Shuffle(10)
|
||||
|
||||
game.Loop()
|
||||
}
|
111
lib/solver.go
Normal file
111
lib/solver.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"15/stack"
|
||||
"math"
|
||||
)
|
||||
|
||||
// https://en.wikipedia.org/wiki/Iterative_deepening_A*
|
||||
// path current search path (acts like a stack)
|
||||
// node current node (last node in current path)
|
||||
// cost the cost to reach current node
|
||||
// f estimated cost of the cheapest path (root..node..goal)
|
||||
// h(node) estimated cost of the cheapest path (node..goal)
|
||||
// cost(node, succ) step cost function
|
||||
// is_goal(node) goal test
|
||||
// successors(node) node expanding function, expand nodes ordered by g + h(node)
|
||||
// ida_star(root) return either NOT_FOUND or a pair with the best path and its cost
|
||||
|
||||
// procedure ida_star(root)
|
||||
// bound := h(root)
|
||||
// path := [root]
|
||||
// loop
|
||||
// t := search(path, 0, bound)
|
||||
// if t = FOUND then return (path, bound)
|
||||
// if t = ∞ then return NOT_FOUND
|
||||
// bound := t
|
||||
// end loop
|
||||
// end procedure
|
||||
|
||||
// function search(path, g, bound)
|
||||
// node := path.last
|
||||
// f := g + h(node)
|
||||
// if f > bound then return f
|
||||
// if is_goal(node) then return FOUND
|
||||
// min := ∞
|
||||
// for succ in successors(node) do
|
||||
// if succ not in path then
|
||||
// path.push(succ)
|
||||
// t := search(path, g + cost(node, succ), bound)
|
||||
// if t = FOUND then return FOUND
|
||||
// if t < min then min := t
|
||||
// path.pop()
|
||||
// end if
|
||||
// end for
|
||||
// return min
|
||||
// end function
|
||||
|
||||
// Iterative deepening A*
|
||||
func Solver(b *Board) (stack.Stack[*Board], int) {
|
||||
bound := b.NeededMoves()
|
||||
path := stack.NewStack[*Board]()
|
||||
path.Push(b)
|
||||
|
||||
for {
|
||||
cost, path, found := search(path, 0, bound)
|
||||
|
||||
if found {
|
||||
return path, bound
|
||||
}
|
||||
|
||||
if cost == math.MaxInt {
|
||||
return path, -1
|
||||
}
|
||||
|
||||
bound = cost
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func search(path stack.Stack[*Board], cost int, bound int) (int, stack.Stack[*Board], bool) {
|
||||
last := path.Peek()
|
||||
estimatedCost := cost + last.NeededMoves()
|
||||
|
||||
if last.SolvedFast() {
|
||||
return cost, path, true
|
||||
}
|
||||
|
||||
if estimatedCost > bound {
|
||||
return estimatedCost, path, false
|
||||
}
|
||||
|
||||
minCost := math.MaxInt
|
||||
|
||||
possibleDirections := last.PossibleDirections()
|
||||
|
||||
for _, direction := range possibleDirections {
|
||||
step := last.Copy()
|
||||
step.Move(direction)
|
||||
|
||||
// TODO: check not only last but every node in the path
|
||||
if *step == *last {
|
||||
continue
|
||||
}
|
||||
|
||||
path.Push(step)
|
||||
|
||||
cost, path, found := search(path, cost+1, bound)
|
||||
|
||||
if found {
|
||||
return cost, path, true
|
||||
}
|
||||
|
||||
if cost < minCost {
|
||||
minCost = cost
|
||||
}
|
||||
|
||||
path.Pop()
|
||||
}
|
||||
|
||||
return minCost, path, false
|
||||
}
|
37
lib/solver_test.go
Normal file
37
lib/solver_test.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package lib
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSolved(t *testing.T) {
|
||||
board := NewBoard()
|
||||
|
||||
path, cost := Solver(board)
|
||||
|
||||
if cost != 0 {
|
||||
t.Error("cost for solved board should be 0")
|
||||
}
|
||||
|
||||
if *path.Pop() != *board {
|
||||
t.Error("root board should be in path")
|
||||
}
|
||||
|
||||
if path.Size() != 1 {
|
||||
t.Error("only one board should be in path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleShuffle(t *testing.T) {
|
||||
board := NewBoard()
|
||||
|
||||
board.Shuffle(10)
|
||||
|
||||
path, cost := Solver(board)
|
||||
|
||||
if cost > 10 {
|
||||
t.Error("it should be more greedy to find optimal path")
|
||||
}
|
||||
|
||||
if path.Pop().grid != SOLVED_GRID {
|
||||
t.Error("last board should be solved")
|
||||
}
|
||||
}
|
57
stack/stack.go
Normal file
57
stack/stack.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package stack
|
||||
|
||||
type Stack[Item any] interface {
|
||||
Push(Item)
|
||||
Pop() Item
|
||||
Peek() Item
|
||||
Size() int
|
||||
IsEmpty() bool
|
||||
ForEach(func(Item))
|
||||
}
|
||||
|
||||
// We use linked list as internal data structure
|
||||
// to get O(1) speed for push and pop operations
|
||||
type node[Item any] struct {
|
||||
item Item
|
||||
next *node[Item]
|
||||
}
|
||||
|
||||
type stack[OfType any] struct {
|
||||
size int
|
||||
head *node[OfType]
|
||||
}
|
||||
|
||||
func NewStack[OfType any]() Stack[OfType] {
|
||||
return &stack[OfType]{}
|
||||
}
|
||||
|
||||
func (s *stack[Item]) Push(item Item) {
|
||||
next := s.head
|
||||
s.head = &node[Item]{item, next}
|
||||
s.size++
|
||||
}
|
||||
|
||||
func (s *stack[Item]) Pop() Item {
|
||||
head := s.head
|
||||
s.head = head.next
|
||||
s.size--
|
||||
return head.item
|
||||
}
|
||||
|
||||
func (s *stack[Item]) Peek() Item {
|
||||
return s.head.item
|
||||
}
|
||||
|
||||
func (s *stack[_]) Size() int {
|
||||
return s.size
|
||||
}
|
||||
|
||||
func (s *stack[_]) IsEmpty() bool {
|
||||
return s.size == 0
|
||||
}
|
||||
|
||||
func (s *stack[Item]) ForEach(f func(Item)) {
|
||||
for walk := s.head; walk != nil; walk = walk.next {
|
||||
f(walk.item)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue