This commit is contained in:
Gregory Tertyshny 2023-07-26 11:30:13 +03:00
parent 41f26363c8
commit e9faf31ac9
10 changed files with 326 additions and 64 deletions

85
cli/game.go Normal file
View 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()
}

View file

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

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

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

View file

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

View file

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

View file

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