529 lines
12 KiB
Go
529 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"../../"
|
|
"github.com/fatih/color"
|
|
termbox "github.com/nsf/termbox-go"
|
|
)
|
|
|
|
func main() {
|
|
var inpFn string
|
|
var done bool
|
|
if inpFn = aoc.GetArgNumber(1); inpFn != "" {
|
|
done = true
|
|
}
|
|
|
|
err := termbox.Init()
|
|
tWidth, tHeight := termbox.Size()
|
|
if err != nil {
|
|
fmt.Println("Error initializing termbox")
|
|
os.Exit(1)
|
|
}
|
|
defer termbox.Close()
|
|
var menuPos int
|
|
cursor := color.New(color.FgBlack).Add(color.BgWhite)
|
|
for !done {
|
|
fmt.Println(aoc.ClearScreen)
|
|
title := color.New(color.FgBlack).Add(color.BgYellow)
|
|
title.Println(CenterText("day 22", tWidth))
|
|
if menuPos == 0 {
|
|
cursor.Println(CenterText("Start Small Game", tWidth))
|
|
} else {
|
|
fmt.Println(CenterText("Start Small Game", tWidth))
|
|
}
|
|
if menuPos == 1 {
|
|
cursor.Println(CenterText("Start Medium Game", tWidth))
|
|
} else {
|
|
fmt.Println(CenterText("Start Medium Game", tWidth))
|
|
}
|
|
if menuPos == 2 {
|
|
cursor.Println(CenterText("Start Large Game", tWidth))
|
|
} else {
|
|
fmt.Println(CenterText("Start Large Game", tWidth))
|
|
}
|
|
if menuPos == 3 {
|
|
cursor.Println(CenterText("Exit", tWidth))
|
|
} else {
|
|
fmt.Println(CenterText("Exit", tWidth))
|
|
}
|
|
fmt.Println()
|
|
fmt.Println(CenterText("To run the AdventOfCode Puzzle, pass the input as an argument to the program:", tWidth))
|
|
fmt.Println(CenterText("./day22 <filename>", tWidth))
|
|
fmt.Println(CenterText("For the part 1 solution:", tWidth))
|
|
fmt.Println(CenterText("./day22 <filename> -1", tWidth))
|
|
ev := termbox.PollEvent()
|
|
if ev.Type == termbox.EventKey {
|
|
switch {
|
|
case ev.Key == termbox.KeyArrowUp || ev.Ch == 'k':
|
|
if menuPos > 0 {
|
|
menuPos--
|
|
}
|
|
case ev.Key == termbox.KeyArrowDown || ev.Ch == 'j':
|
|
if menuPos < 3 {
|
|
menuPos++
|
|
}
|
|
case ev.Key == termbox.KeyEnter:
|
|
done = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default to small game
|
|
sizeX, sizeY := 6, 6
|
|
switch menuPos {
|
|
case 1:
|
|
// Generate a medium game
|
|
sizeX, sizeY = 10, 10
|
|
case 2:
|
|
// Generate a large game
|
|
sizeX, sizeY = 15, 15
|
|
case 3:
|
|
return
|
|
}
|
|
|
|
// Create the display
|
|
d := CreateDisplay(tWidth, tHeight)
|
|
if inpFn != "" {
|
|
input := aoc.FileToStringSlice(inpFn)
|
|
d.reset(CreateNodesFromInput(input))
|
|
} else {
|
|
d.reset(GenerateNodesForGrid(sizeX, sizeY))
|
|
// Grid is generated, designate one node as start (set used = 0)
|
|
stX, stY := rand.Intn(sizeX), rand.Intn(sizeY)
|
|
d.nodes[iToLoc(stX, stY)].Used = 0
|
|
d.cursX, d.cursY = stX, stY
|
|
d.goalX, d.goalY = rand.Intn(sizeX), rand.Intn(sizeY)
|
|
d.nodes[iToLoc(d.goalX, d.goalY)].Used = rand.Intn(75)
|
|
}
|
|
|
|
if aoc.ArgIsSet("-1") {
|
|
var viablePairs []string
|
|
for _, v1 := range d.nodes {
|
|
for _, v2 := range d.nodes {
|
|
if ok, _ := IsViablePair(v1, v2); ok {
|
|
viablePairs = AddViablePair(v1, v2, viablePairs)
|
|
}
|
|
}
|
|
}
|
|
fmt.Println("Viable Pairs:", len(viablePairs))
|
|
return
|
|
}
|
|
|
|
done = false
|
|
|
|
for !done {
|
|
fmt.Println(aoc.ClearScreen)
|
|
d.PrintGrid()
|
|
ev := termbox.PollEvent()
|
|
if d.goalX == 0 && d.goalY == 0 {
|
|
if ev.Type == termbox.EventKey {
|
|
done = true
|
|
}
|
|
} else {
|
|
if ev.Type == termbox.EventResize {
|
|
d.screenWidth, d.screenHeight = termbox.Size()
|
|
} else if ev.Type == termbox.EventKey {
|
|
switch {
|
|
case ev.Key == termbox.KeyArrowUp || ev.Ch == 'k':
|
|
d.resetError()
|
|
if d.cursorMode {
|
|
d.moveCursorUp()
|
|
} else {
|
|
d.moveDataDown()
|
|
}
|
|
case ev.Key == termbox.KeyArrowRight || ev.Ch == 'l':
|
|
d.resetError()
|
|
if d.cursorMode {
|
|
d.moveCursorRight()
|
|
} else {
|
|
d.moveDataLeft()
|
|
}
|
|
case ev.Key == termbox.KeyArrowDown || ev.Ch == 'j':
|
|
d.resetError()
|
|
if d.cursorMode {
|
|
d.moveCursorDown()
|
|
} else {
|
|
d.moveDataUp()
|
|
}
|
|
case ev.Key == termbox.KeyArrowLeft || ev.Ch == 'h':
|
|
d.resetError()
|
|
if d.cursorMode {
|
|
d.moveCursorLeft()
|
|
} else {
|
|
d.moveDataRight()
|
|
}
|
|
case ev.Ch == 'q':
|
|
done = true
|
|
case ev.Key == termbox.KeySpace:
|
|
d.switchMode()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func GenerateNodesForGrid(w, h int) (map[string]*Node, int, int) {
|
|
nodes := make(map[string]*Node)
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
// It doesn't play well if we have totals that are less than half(?) the highest
|
|
for y := 0; y <= h; y++ {
|
|
for x := 0; x <= w; x++ {
|
|
ttl := 20 + rand.Intn(100-20)
|
|
used := rand.Intn(ttl)
|
|
n := CreateNode(x, y, ttl, used)
|
|
nodes[iToLoc(x, y)] = n
|
|
}
|
|
}
|
|
return nodes, w, h
|
|
}
|
|
|
|
// Returns the node map and the max X and max Y values
|
|
func CreateNodesFromInput(input []string) (map[string]*Node, int, int) {
|
|
nodes := make(map[string]*Node)
|
|
var maxX, maxY int
|
|
for i := range input {
|
|
if input[i] == "" {
|
|
continue
|
|
}
|
|
if input[i][:9] == "/dev/grid" {
|
|
var n *Node
|
|
var err error
|
|
if n, err = CreateNodeFromDfListing(input[i]); err != nil {
|
|
fmt.Println(err)
|
|
continue
|
|
}
|
|
nodes[aoc.Itoa(n.X)+";"+aoc.Itoa(n.Y)] = n
|
|
if n.X > maxX {
|
|
maxX = n.X
|
|
}
|
|
if n.Y > maxY {
|
|
maxY = n.Y
|
|
}
|
|
}
|
|
}
|
|
return nodes, maxX, maxY
|
|
}
|
|
|
|
type Display struct {
|
|
screenHeight, screenWidth int
|
|
cursX, cursY int
|
|
goalX, goalY int
|
|
maxX, maxY int
|
|
errorX, errorY int
|
|
nodes map[string]*Node
|
|
// Cursor Mode means the user can move the cursor around
|
|
// Otherwise the screen locks on the node with 0 used
|
|
cursorMode bool
|
|
// moveCount tracks how many times we've moved data
|
|
moveCount int
|
|
|
|
message string
|
|
}
|
|
|
|
func CreateDisplay(h, w int) *Display {
|
|
d := new(Display)
|
|
d.screenHeight = h
|
|
d.screenWidth = w
|
|
d.errorX, d.errorY = -1, -1
|
|
return d
|
|
}
|
|
|
|
func (d *Display) resetError() {
|
|
d.errorX, d.errorY = -1, -1
|
|
}
|
|
|
|
func (d *Display) reset(n map[string]*Node, mX, mY int) {
|
|
d.nodes = n
|
|
d.maxX, d.maxY = mX, mY
|
|
d.cursX, d.cursY = 0, 0
|
|
d.goalX, d.goalY = d.maxX, 0
|
|
d.errorX, d.errorY = -1, -1
|
|
d.message = ""
|
|
d.cursorMode = true
|
|
d.switchMode()
|
|
}
|
|
|
|
func (d *Display) switchMode() {
|
|
d.cursorMode = !d.cursorMode
|
|
if !d.cursorMode {
|
|
// Find the empty node and set the cursor to it
|
|
for _, v := range d.nodes {
|
|
if v.Used == 0 {
|
|
d.cursX, d.cursY = v.X, v.Y
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Display) moveCursorUp() {
|
|
if d.cursY > 0 {
|
|
d.cursY--
|
|
}
|
|
}
|
|
|
|
func (d *Display) moveCursorRight() {
|
|
if d.cursX < d.maxX {
|
|
d.cursX++
|
|
}
|
|
}
|
|
|
|
func (d *Display) moveCursorDown() {
|
|
if d.cursY < d.maxY {
|
|
d.cursY++
|
|
}
|
|
}
|
|
|
|
func (d *Display) moveCursorLeft() {
|
|
if d.cursX > 0 {
|
|
d.cursX--
|
|
}
|
|
}
|
|
|
|
func (d *Display) moveDataUp() {
|
|
if d.cursY >= d.maxY {
|
|
d.message = "Invalid Request"
|
|
return
|
|
}
|
|
src := iToLoc(d.cursX, d.cursY+1)
|
|
dest := iToLoc(d.cursX, d.cursY)
|
|
if ok, err := IsViablePair(d.nodes[src], d.nodes[dest]); ok {
|
|
d.nodes[dest].Used = d.nodes[src].Used
|
|
d.nodes[src].Used = 0
|
|
d.cursY++
|
|
d.message = "Data Moved"
|
|
d.moveCount++
|
|
if src == iToLoc(d.goalX, d.goalY) {
|
|
d.goalY--
|
|
}
|
|
} else {
|
|
d.errorX, d.errorY = d.cursX, d.cursY+1
|
|
d.message = fmt.Sprintf(
|
|
"(%d;%d)[%3d/%3d] (%d;%d)[%3d/%3d] %s",
|
|
d.nodes[src].X, d.nodes[src].Y, d.nodes[src].Used, d.nodes[src].Size,
|
|
d.nodes[dest].X, d.nodes[dest].Y, d.nodes[dest].Used, d.nodes[dest].Size,
|
|
err.Error(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (d *Display) moveDataRight() {
|
|
if d.cursX <= 0 {
|
|
d.message = "Invalid Request"
|
|
return
|
|
}
|
|
src := iToLoc(d.cursX-1, d.cursY)
|
|
dest := iToLoc(d.cursX, d.cursY)
|
|
if ok, err := IsViablePair(d.nodes[src], d.nodes[dest]); ok {
|
|
d.nodes[dest].Used = d.nodes[src].Used
|
|
d.nodes[src].Used = 0
|
|
d.cursX--
|
|
d.message = "Data Moved"
|
|
d.moveCount++
|
|
if src == iToLoc(d.goalX, d.goalY) {
|
|
d.goalX++
|
|
}
|
|
} else {
|
|
d.errorX, d.errorY = d.cursX-1, d.cursY
|
|
d.message = fmt.Sprintf(
|
|
"(%d;%d)[%3d/%3d] (%d;%d)[%3d/%3d] %s",
|
|
d.nodes[src].X, d.nodes[src].Y, d.nodes[src].Used, d.nodes[src].Size,
|
|
d.nodes[dest].X, d.nodes[dest].Y, d.nodes[dest].Used, d.nodes[dest].Size,
|
|
err.Error(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (d *Display) moveDataDown() {
|
|
if d.cursY <= 0 {
|
|
d.message = "Invalid Request"
|
|
return
|
|
}
|
|
src := iToLoc(d.cursX, d.cursY-1)
|
|
dest := iToLoc(d.cursX, d.cursY)
|
|
if ok, err := IsViablePair(d.nodes[src], d.nodes[dest]); ok {
|
|
d.nodes[dest].Used = d.nodes[src].Used
|
|
d.nodes[src].Used = 0
|
|
d.cursY--
|
|
d.message = "Data Moved"
|
|
d.moveCount++
|
|
if src == iToLoc(d.goalX, d.goalY) {
|
|
d.goalY++
|
|
}
|
|
} else {
|
|
d.errorX, d.errorY = d.cursX, d.cursY-1
|
|
d.message = fmt.Sprintf(
|
|
"(%d;%d)[%3d/%3d] (%d;%d)[%3d/%3d] %s",
|
|
d.nodes[src].X, d.nodes[src].Y, d.nodes[src].Used, d.nodes[src].Size,
|
|
d.nodes[dest].X, d.nodes[dest].Y, d.nodes[dest].Used, d.nodes[dest].Size,
|
|
err.Error(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (d *Display) moveDataLeft() {
|
|
if d.cursX >= d.maxX {
|
|
d.message = "Invalid Request"
|
|
return
|
|
}
|
|
src := iToLoc(d.cursX+1, d.cursY)
|
|
dest := iToLoc(d.cursX, d.cursY)
|
|
if ok, err := IsViablePair(d.nodes[src], d.nodes[dest]); ok {
|
|
d.nodes[dest].Used = d.nodes[src].Used
|
|
d.nodes[src].Used = 0
|
|
d.cursX++
|
|
d.message = "Data Moved"
|
|
d.moveCount++
|
|
if src == iToLoc(d.goalX, d.goalY) {
|
|
d.goalX--
|
|
}
|
|
} else {
|
|
d.errorX, d.errorY = d.cursX+1, d.cursY
|
|
d.message = fmt.Sprintf(
|
|
"(%d;%d)[%3d/%3d] (%d;%d)[%3d/%3d] %s",
|
|
d.nodes[src].X, d.nodes[src].Y, d.nodes[src].Used, d.nodes[src].Size,
|
|
d.nodes[dest].X, d.nodes[dest].Y, d.nodes[dest].Used, d.nodes[dest].Size,
|
|
err.Error(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (d *Display) PrintGrid() {
|
|
cursor := color.New(color.FgBlack).Add(color.BgWhite)
|
|
if !d.cursorMode {
|
|
cursor = color.New(color.FgBlack).Add(color.BgYellow)
|
|
}
|
|
goal := color.New(color.FgGreen)
|
|
error := color.New(color.FgRed)
|
|
df := color.New(color.FgWhite)
|
|
if d.goalX == 0 && d.goalY == 0 {
|
|
df = color.New(color.FgYellow)
|
|
}
|
|
// Each cell takes up 9 width
|
|
// We can only fit about (d.screenWidth / 9) cells wide
|
|
// and d.screenHeight tall
|
|
//dispWidth := d.screenWidth / 9
|
|
numCellsW := d.screenWidth / 3
|
|
leftMost, rightMost := d.cursX-(numCellsW/2), d.cursX+(numCellsW/2)
|
|
for leftMost < 0 {
|
|
leftMost++
|
|
rightMost++
|
|
}
|
|
if rightMost > d.maxX {
|
|
rightMost = d.maxX
|
|
}
|
|
|
|
df.Print(" ")
|
|
for j := leftMost; j <= rightMost; j++ {
|
|
df.Printf("|%7d|", j)
|
|
}
|
|
df.Println()
|
|
for i := 0; i <= d.maxY; i++ {
|
|
df.Printf("|%2d|", i)
|
|
for j := leftMost; j <= rightMost; j++ {
|
|
n := d.nodes[iToLoc(j, i)]
|
|
if j == d.cursX && i == d.cursY {
|
|
cursor.Printf("[%3d/%3d]", n.Used, n.Size)
|
|
} else if j == d.errorX && i == d.errorY {
|
|
error.Printf("[%3d/%3d]", n.Used, n.Size)
|
|
} else if j == d.goalX && i == d.goalY {
|
|
goal.Printf("[%3d/%3d]", n.Used, n.Size)
|
|
} else {
|
|
if n != nil {
|
|
df.Printf("[%3d/%3d]", n.Used, n.Size)
|
|
} else {
|
|
df.Print("[ / ]")
|
|
}
|
|
}
|
|
}
|
|
df.Println()
|
|
}
|
|
df.Println(d.message)
|
|
df.Println("Moves:", d.moveCount, "Goal Data at:", iToLoc(d.goalX, d.goalY))
|
|
if d.cursorMode {
|
|
cursor.Println("Cursor Mode")
|
|
} else {
|
|
cursor.Println("Data Move Mode")
|
|
}
|
|
df.Println("(q): quit, (space): switch mode")
|
|
}
|
|
|
|
func iToLoc(x, y int) string {
|
|
return aoc.Itoa(x) + ";" + aoc.Itoa(y)
|
|
}
|
|
|
|
func AddViablePair(a, b *Node, list []string) []string {
|
|
// Make sure this pair isn't already in the list
|
|
for i := range list {
|
|
if list[i] == a.GetLocString()+"=>"+b.GetLocString() || list[i] == b.GetLocString()+"=>"+a.GetLocString() {
|
|
return list
|
|
}
|
|
}
|
|
return append(list, a.GetLocString()+"=>"+b.GetLocString())
|
|
}
|
|
|
|
func IsViablePair(a, b *Node) (bool, error) {
|
|
fmt.Println(" Initiating Viable Pair Check:", a.GetLocString(), b.GetLocString())
|
|
if a == b {
|
|
return false, errors.New("They're the same node")
|
|
}
|
|
//if a.Used == 0 {
|
|
// return false, errors.New(a.GetLocString() + " has no data")
|
|
//}
|
|
if a.Used > b.Size-b.Used {
|
|
return false, errors.New(b.GetLocString() + " doesn't have enough space")
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
type Node struct {
|
|
X, Y int
|
|
Size, Used int
|
|
}
|
|
|
|
func CreateNode(x, y, size, used int) *Node {
|
|
n := new(Node)
|
|
n.X, n.Y = x, y
|
|
n.Size, n.Used = size, used
|
|
return n
|
|
}
|
|
|
|
func CreateNodeFromDfListing(inp string) (*Node, error) {
|
|
pts := strings.Fields(inp)
|
|
if len(pts) != 5 {
|
|
return nil, errors.New("Error Creating Node: Invalid DF Listing")
|
|
}
|
|
n := new(Node)
|
|
parseLoc := strings.Split(pts[0], "-")
|
|
if parseLoc[1][0] == 'x' {
|
|
n.X = aoc.Atoi(parseLoc[1][1:])
|
|
}
|
|
if parseLoc[2][0] == 'y' {
|
|
n.Y = aoc.Atoi(parseLoc[2][1:])
|
|
}
|
|
n.Size = aoc.Atoi(strings.TrimSuffix(pts[1], "T"))
|
|
n.Used = aoc.Atoi(strings.TrimSuffix(pts[2], "T"))
|
|
|
|
return n, nil
|
|
}
|
|
|
|
func (n *Node) GetLocString() string {
|
|
return aoc.Itoa(n.X) + ";" + aoc.Itoa(n.Y)
|
|
}
|
|
|
|
func (n *Node) ToString() string {
|
|
return fmt.Sprint("[", n.GetLocString(), "](S:", aoc.Itoa(n.Size), ";A:", aoc.Itoa(n.Size-n.Used), ";U:", aoc.Itoa(n.Used), ")")
|
|
}
|
|
|
|
func CenterText(txt string, width int) string {
|
|
width -= len(txt)
|
|
return strings.Repeat(" ", width/2) + txt + strings.Repeat(" ", width/2)
|
|
}
|