From e9faf31ac9be75b1ae2838b78becd6ef0be4be07 Mon Sep 17 00:00:00 2001 From: Gregory Tertyshny Date: Wed, 26 Jul 2023 11:30:13 +0300 Subject: [PATCH] solver --- cli/game.go | 85 ++++++++++++++++++++++++++++++++++ cli/main.go | 20 ++++++-- go.mod | 1 + go.sum | 2 + lib/board.go | 11 ++++- lib/board_test.go | 8 ++++ lib/game.go | 58 ----------------------- lib/solver.go | 111 +++++++++++++++++++++++++++++++++++++++++++++ lib/solver_test.go | 37 +++++++++++++++ stack/stack.go | 57 +++++++++++++++++++++++ 10 files changed, 326 insertions(+), 64 deletions(-) create mode 100644 cli/game.go delete mode 100644 lib/game.go create mode 100644 lib/solver.go create mode 100644 lib/solver_test.go create mode 100644 stack/stack.go diff --git a/cli/game.go b/cli/game.go new file mode 100644 index 0000000..1c15b32 --- /dev/null +++ b/cli/game.go @@ -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() +} diff --git a/cli/main.go b/cli/main.go index 19730e1..1780aa1 100644 --- a/cli/main.go +++ b/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) + } } diff --git a/go.mod b/go.mod index bafcf9c..3d4c2da 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index d0f772d..5a29769 100644 --- a/go.sum +++ b/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= diff --git a/lib/board.go b/lib/board.go index 84e39f3..266824f 100644 --- a/lib/board.go +++ b/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++ { diff --git a/lib/board_test.go b/lib/board_test.go index 16d0f42..05123ed 100644 --- a/lib/board_test.go +++ b/lib/board_test.go @@ -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) { diff --git a/lib/game.go b/lib/game.go deleted file mode 100644 index 5d84d5d..0000000 --- a/lib/game.go +++ /dev/null @@ -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() -} diff --git a/lib/solver.go b/lib/solver.go new file mode 100644 index 0000000..a873d0b --- /dev/null +++ b/lib/solver.go @@ -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 +} diff --git a/lib/solver_test.go b/lib/solver_test.go new file mode 100644 index 0000000..d0d1b5a --- /dev/null +++ b/lib/solver_test.go @@ -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") + } +} diff --git a/stack/stack.go b/stack/stack.go new file mode 100644 index 0000000..3fe1747 --- /dev/null +++ b/stack/stack.go @@ -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) + } +}