Compare commits
No commits in common. "aec72d381eea0546ffa7383e4527a2e86b9767bc" and "b8757ef8e54ccb11f06e0f79c82ede422421feec" have entirely different histories.
aec72d381e
...
b8757ef8e5
12 changed files with 11 additions and 768 deletions
15
Dockerfile
15
Dockerfile
|
@ -1,15 +0,0 @@
|
|||
FROM golang:1.20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go test ./...
|
||||
|
||||
RUN cd ./cli && CGO_ENABLED=0 GOOS=linux go build -o 15
|
||||
|
||||
ENTRYPOINT ["./cli/15"]
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 fotonmoton
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
README.md
23
README.md
|
@ -1,22 +1,3 @@
|
|||
# 15 puzzle
|
||||
Simple 15 puzzle implementation with naive solver.
|
||||
# 15
|
||||
|
||||
## Docker
|
||||
|
||||
```
|
||||
docker run --rm -it $(docker build -q .) game
|
||||
```
|
||||
or
|
||||
```
|
||||
docker run --rm -it $(docker build -q .) solve
|
||||
```
|
||||
|
||||
## From source
|
||||
|
||||
```
|
||||
cd cli && go run . solve
|
||||
```
|
||||
or
|
||||
```
|
||||
cd cli && go run . game
|
||||
```
|
||||
15 puzzle
|
85
cli/game.go
85
cli/game.go
|
@ -1,85 +0,0 @@
|
|||
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()
|
||||
}
|
21
cli/main.go
21
cli/main.go
|
@ -1,21 +0,0 @@
|
|||
package main
|
||||
|
||||
import "os"
|
||||
|
||||
func main() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
15
go.mod
15
go.mod
|
@ -1,15 +0,0 @@
|
|||
module 15
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
atomicgo.dev/keyboard v0.2.9
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090
|
||||
)
|
||||
|
||||
require github.com/vcaesar/keycode v0.10.0 // indirect
|
||||
|
||||
require (
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
)
|
59
go.sum
59
go.sum
|
@ -1,59 +0,0 @@
|
|||
atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=
|
||||
atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=
|
||||
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
|
||||
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
|
||||
github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=
|
||||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
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/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=
|
||||
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
|
||||
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
|
||||
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
|
||||
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||
github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/vcaesar/keycode v0.10.0 h1:Qx5QE8ZXHyRyjoA2QOxBp25OKMKB+zxMVqm0FWGV0d4=
|
||||
github.com/vcaesar/keycode v0.10.0/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
217
lib/board.go
217
lib/board.go
|
@ -1,217 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type Board struct {
|
||||
grid [16]int
|
||||
empty [2]int
|
||||
}
|
||||
|
||||
const ROW_COUNT = 4
|
||||
|
||||
var SOLVED_GRID = [16]int{
|
||||
1, 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
13, 14, 15, 0,
|
||||
}
|
||||
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
LEFT Direction = iota
|
||||
RIGHT
|
||||
UP
|
||||
DOWN
|
||||
)
|
||||
|
||||
func NewBoard() *Board {
|
||||
return &Board{
|
||||
grid: [16]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0},
|
||||
empty: [2]int{3, 3},
|
||||
}
|
||||
}
|
||||
|
||||
// If all pieces on their desired places
|
||||
// no more moves are needed and we can say that
|
||||
// board is solved.
|
||||
func (board *Board) Solved() bool {
|
||||
return board.NeededMoves() == 0
|
||||
}
|
||||
|
||||
// Faster way to check if board is solved.
|
||||
// Arrays are comparable in Go so we can simply
|
||||
// compare desired state with current
|
||||
func (board *Board) SolvedFast() bool {
|
||||
return board.grid == SOLVED_GRID
|
||||
}
|
||||
|
||||
func (b *Board) Print() {
|
||||
for i, cell := range b.grid {
|
||||
if cell == 0 {
|
||||
fmt.Printf(" ")
|
||||
} else {
|
||||
fmt.Printf("%3d", cell)
|
||||
}
|
||||
if i == 3 || i == 7 || i == 11 {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func (b *Board) PossibleDirections() []Direction {
|
||||
directions := []Direction{}
|
||||
|
||||
if b.empty[0] != 0 {
|
||||
directions = append(directions, UP)
|
||||
}
|
||||
|
||||
if b.empty[0] != 3 {
|
||||
directions = append(directions, DOWN)
|
||||
}
|
||||
|
||||
if b.empty[1] != 0 {
|
||||
directions = append(directions, LEFT)
|
||||
}
|
||||
|
||||
if b.empty[1] != 3 {
|
||||
directions = append(directions, RIGHT)
|
||||
}
|
||||
|
||||
return directions
|
||||
}
|
||||
|
||||
// "Moves" empty cell to new position.
|
||||
// It's easier to reason if we will move
|
||||
// empty cell as another piece rather than
|
||||
// moving pieces that are surrounds it.
|
||||
func (b *Board) Move(d Direction) {
|
||||
possibleDirections := b.PossibleDirections()
|
||||
|
||||
if slices.Index(possibleDirections, d) == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
toRow, toCol := directionToStep(d)
|
||||
|
||||
newRow := b.empty[0] + toRow
|
||||
newCol := b.empty[1] + toCol
|
||||
|
||||
piceToSwap := b.get(newRow, newCol)
|
||||
|
||||
b.set(b.empty[0], b.empty[1], piceToSwap)
|
||||
b.set(newRow, newCol, 0)
|
||||
|
||||
b.empty[0] = newRow
|
||||
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{}
|
||||
|
||||
for i := 0; i < steps; i++ {
|
||||
possibleDirections := b.PossibleDirections()
|
||||
|
||||
// Remove opposite moves to prevent
|
||||
// moving around one cell
|
||||
if len(moves) != 0 {
|
||||
last := moves[len(moves)-1]
|
||||
possibleDirections = slices.DeleteFunc(
|
||||
possibleDirections,
|
||||
func(direction Direction) bool {
|
||||
return oppositeDirections(direction, last)
|
||||
})
|
||||
}
|
||||
|
||||
rand.Shuffle(len(possibleDirections), func(i, j int) {
|
||||
possibleDirections[i], possibleDirections[j] = possibleDirections[j], possibleDirections[i]
|
||||
})
|
||||
|
||||
nextMove := possibleDirections[0]
|
||||
|
||||
b.Move(nextMove)
|
||||
|
||||
moves = append(moves, nextMove)
|
||||
}
|
||||
|
||||
return moves
|
||||
}
|
||||
|
||||
// Optimistic number. Indicates
|
||||
// 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 {
|
||||
neededMoves := 0
|
||||
|
||||
for row := 0; row < 4; row++ {
|
||||
for col := 0; col < 4; col++ {
|
||||
|
||||
number := board.get(row, col)
|
||||
|
||||
if number == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
neededMoves += rectilinearDistance(number, row, col)
|
||||
}
|
||||
}
|
||||
|
||||
return neededMoves
|
||||
}
|
||||
|
||||
func (b *Board) get(row, col int) int {
|
||||
return b.grid[row*ROW_COUNT+col]
|
||||
}
|
||||
|
||||
func (b *Board) set(row, col, val int) {
|
||||
b.grid[row*ROW_COUNT+col] = val
|
||||
}
|
||||
|
||||
func originalPosition(number int) (int, int) {
|
||||
return (number - 1) / 4, (number - 1) % 4
|
||||
}
|
||||
|
||||
// Or "Manhattan distance". We use it to calculate "shortest" path
|
||||
// to desired piece position.
|
||||
// https://en.wikipedia.org/wiki/Taxicab_geometry
|
||||
func rectilinearDistance(number, i, j int) int {
|
||||
origRow, origCol := originalPosition(number)
|
||||
return int(math.Abs(float64(origRow-i)) + math.Abs(float64(origCol-j)))
|
||||
}
|
||||
|
||||
func directionToStep(d Direction) (int, int) {
|
||||
switch d {
|
||||
case UP:
|
||||
return -1, 0
|
||||
case DOWN:
|
||||
return 1, 0
|
||||
case LEFT:
|
||||
return 0, -1
|
||||
case RIGHT:
|
||||
return 0, 1
|
||||
default:
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
func oppositeDirections(a Direction, b Direction) bool {
|
||||
ar, al := directionToStep(a)
|
||||
br, bl := directionToStep(b)
|
||||
|
||||
return ar+br == 0 && al+bl == 0
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func TestSolvedState(t *testing.T) {
|
||||
board := NewBoard()
|
||||
if !board.Solved() {
|
||||
t.Error("Initial state should be solved")
|
||||
}
|
||||
|
||||
boardWithShuffledPieces := Board{grid: [16]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 0, 15}, empty: [2]int{3, 3}}
|
||||
|
||||
if boardWithShuffledPieces.Solved() {
|
||||
t.Error("Shuffled board should not be solved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolvedFast(t *testing.T) {
|
||||
board := NewBoard()
|
||||
if !board.SolvedFast() {
|
||||
t.Error("Initial state should be solved")
|
||||
}
|
||||
|
||||
boardWithShuffledPieces := Board{
|
||||
grid: [16]int{
|
||||
1, 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
13, 14, 0, 15,
|
||||
},
|
||||
empty: [2]int{3, 3}}
|
||||
|
||||
if boardWithShuffledPieces.SolvedFast() {
|
||||
t.Error("Shuffled board should not be solved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPossibleDirections(t *testing.T) {
|
||||
board := NewBoard()
|
||||
directions := board.PossibleDirections()
|
||||
|
||||
if len(directions) != 2 {
|
||||
t.Error("For initial state only UP and LEFT directions should be available")
|
||||
}
|
||||
|
||||
isUPresent := slices.Index(directions, UP)
|
||||
isLeftPresent := slices.Index(directions, LEFT)
|
||||
|
||||
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) {
|
||||
board := NewBoard()
|
||||
board.Move(LEFT)
|
||||
|
||||
toTheRight := board.grid == [16]int{
|
||||
1, 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
13, 14, 0, 15,
|
||||
}
|
||||
|
||||
if !toTheRight {
|
||||
t.Error("Move should move pieces")
|
||||
}
|
||||
|
||||
if board.empty != [2]int{3, 2} {
|
||||
t.Error("after Move new empty position should be set")
|
||||
}
|
||||
|
||||
board.Move(UP)
|
||||
board.Move(UP)
|
||||
board.Move(UP)
|
||||
|
||||
tripleUp := board.grid == [16]int{
|
||||
1, 2, 0, 4,
|
||||
5, 6, 3, 8,
|
||||
9, 10, 7, 12,
|
||||
13, 14, 11, 15,
|
||||
}
|
||||
|
||||
if !tripleUp {
|
||||
t.Error("Corrupt state after moving")
|
||||
}
|
||||
|
||||
board.Move(UP)
|
||||
|
||||
asBefore := board.grid == [16]int{
|
||||
1, 2, 0, 4,
|
||||
5, 6, 3, 8,
|
||||
9, 10, 7, 12,
|
||||
13, 14, 11, 15,
|
||||
}
|
||||
|
||||
if !asBefore {
|
||||
t.Error("If we cannot move further state should stay the same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOppositeDirections(t *testing.T) {
|
||||
vertical := oppositeDirections(UP, DOWN)
|
||||
horizontal := oppositeDirections(LEFT, RIGHT)
|
||||
|
||||
if !vertical || !horizontal {
|
||||
t.Error("Opposite direction should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShuffle(t *testing.T) {
|
||||
board := NewBoard()
|
||||
board.Shuffle(2)
|
||||
|
||||
if board.SolvedFast() {
|
||||
t.Error("Board should be in unsolved state after shuffle")
|
||||
}
|
||||
}
|
111
lib/solver.go
111
lib/solver.go
|
@ -1,111 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
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.Peek() != *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")
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
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