base cli game

This commit is contained in:
Gregory Tertyshny 2023-07-25 11:20:51 +03:00
commit 41f26363c8
6 changed files with 472 additions and 0 deletions

9
cli/main.go Normal file
View file

@ -0,0 +1,9 @@
package main
import (
"15/lib"
)
func main() {
lib.Start()
}

15
go.mod Normal file
View file

@ -0,0 +1,15 @@
module 15
go 1.20
require (
atomicgo.dev/keyboard v0.2.9
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
)
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
)

58
go.sum Normal file
View file

@ -0,0 +1,58 @@
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/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-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/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=

210
lib/board.go Normal file
View file

@ -0,0 +1,210 @@
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) 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
}

122
lib/board_test.go Normal file
View file

@ -0,0 +1,122 @@
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")
}
}
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")
}
}

58
lib/game.go Normal file
View file

@ -0,0 +1,58 @@
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()
}