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
|
package main
|
||||||
|
|
||||||
import (
|
import "os"
|
||||||
"15/lib"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
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 (
|
require (
|
||||||
github.com/containerd/console v1.0.3 // indirect
|
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
|
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/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.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/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.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||||
github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=
|
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=
|
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
|
// no more moves are needed and we can say that
|
||||||
// board is solved.
|
// board is solved.
|
||||||
func (board *Board) Solved() bool {
|
func (board *Board) Solved() bool {
|
||||||
return board.neededMoves() == 0
|
return board.NeededMoves() == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Faster way to check if board is solved.
|
// Faster way to check if board is solved.
|
||||||
|
@ -113,6 +113,13 @@ func (b *Board) Move(d Direction) {
|
||||||
b.empty[1] = newCol
|
b.empty[1] = newCol
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Board) Copy() *Board {
|
||||||
|
return &Board{
|
||||||
|
grid: b.grid,
|
||||||
|
empty: b.empty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Board) Shuffle(steps int) []Direction {
|
func (b *Board) Shuffle(steps int) []Direction {
|
||||||
moves := []Direction{}
|
moves := []Direction{}
|
||||||
|
|
||||||
|
@ -148,7 +155,7 @@ func (b *Board) Shuffle(steps int) []Direction {
|
||||||
// sum of number of moves each board piece should do
|
// sum of number of moves each board piece should do
|
||||||
// to get to desired position. It ignores real "circular"
|
// to get to desired position. It ignores real "circular"
|
||||||
// moves and calculates moves as if only one piece exists on the board.
|
// 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
|
neededMoves := 0
|
||||||
|
|
||||||
for row := 0; row < 4; row++ {
|
for row := 0; row < 4; row++ {
|
||||||
|
|
|
@ -53,6 +53,14 @@ func TestPossibleDirections(t *testing.T) {
|
||||||
if isLeftPresent == -1 || isUPresent == -1 {
|
if isLeftPresent == -1 || isUPresent == -1 {
|
||||||
t.Error("UP and LEFT directions should be present")
|
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) {
|
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