15/lib/board.go

218 lines
4.1 KiB
Go
Raw Normal View History

2023-07-25 11:20:51 +03:00
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 {
2023-07-26 11:30:13 +03:00
return board.NeededMoves() == 0
2023-07-25 11:20:51 +03:00
}
// 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
}
2023-07-26 11:30:13 +03:00
func (b *Board) Copy() *Board {
return &Board{
grid: b.grid,
empty: b.empty,
}
}
2023-07-25 11:20:51 +03:00
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.
2023-07-26 11:30:13 +03:00
func (board *Board) NeededMoves() int {
2023-07-25 11:20:51 +03:00
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
}