adventofcode/2019/day25/screen.go

405 lines
9.4 KiB
Go

package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
intcode "git.bullercodeworks.com/brian/adventofcode/2019/intcode-processor"
termboxScreen "github.com/br0xen/termbox-screen"
termboxUtil "github.com/br0xen/termbox-util"
"github.com/nsf/termbox-go"
)
const (
ModeWaiting = iota
ModeInput
)
const (
Bg = termbox.ColorBlack
Fg = termbox.ColorGreen
)
var spinner []rune
type Screen struct {
Title string
Program *intcode.Program
LastPI int
Mode int
Events *EventBuffer
Cli *CliProc
TheLog []string
LogStartIdx int
LogDisplayLines int
StatusText string
StatusSpinner int
Inventory []string
id int
}
func NewScreen(p *intcode.Program) *Screen {
s := Screen{
Title: "Day 25: Cryostasis",
Program: p,
Mode: ModeWaiting,
Events: NewEventBuffer(),
Cli: NewCLI(),
StatusSpinner: 0,
}
spinner = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█', '▇', '▆', '▅', '▄', '▃', '▁'}
// Process to update screen from program
go func() {
for {
if p.WaitingForInput || p.State == intcode.RET_DONE {
if !s.Cli.Complete {
s.Mode = ModeInput
} else {
s.Mode = ModeWaiting
inp := s.Cli.GetBuffer()
s.Cli.ClearBuffer()
if s.IsEngineCommand(inp) {
if err := s.ProcessCommand(inp); err != nil {
s.Logln("Engine Error: " + err.Error())
}
continue
}
for k := range inp {
for !p.WaitingForInput {
time.Sleep(1)
}
s.StatusText = "Transmitting " + inp[0:k]
s.Log(string(inp[k]))
s.Program.Input(int(inp[k]))
}
s.StatusText = "Please Wait..."
s.Program.Input(int('\n'))
s.Logln("")
if strings.HasPrefix(inp, "take ") {
s.Cli.RemoveSuggestion(inp)
invAdd := strings.TrimPrefix(inp, "take ")
s.Inventory = append(s.Inventory, invAdd)
} else if strings.HasPrefix(inp, "drop ") {
invRem := strings.TrimPrefix(inp, "drop ")
for i := range s.Inventory {
if s.Inventory[i] == invRem {
s.Inventory = append(s.Inventory[:i], s.Inventory[i+1:]...)
break
}
}
}
}
}
if p.WaitingForOutput {
v := s.Program.Output()
s.StatusText = "Receiving '" + string(v) + "'"
if v == '\n' {
s.Logln("")
} else {
s.Log(string(v))
}
}
}
}()
// And actually run the program
s.Logln("Welcome to the Day25 Cryostasis Engine!")
s.Logln("Type '/start' to start")
s.Logln("Type '/load <save>' to load a save")
go func() {
ret := p.Run()
if ret == intcode.RET_DONE {
s.Logln("Program is done")
} else if ret == intcode.RET_ERR {
s.Logln("Program errored")
}
s.Logln("Type '/quit' to quit")
}()
return &s
}
func (s *Screen) Id() int { return s.id }
func (s *Screen) Initialize(bundle termboxScreen.Bundle) error {
s.Events.Clear()
s.TheLog = nil
return nil
}
func (s *Screen) HandleNoneEvent(event termbox.Event) int {
// Update the display
return s.Id()
}
func (s *Screen) HandleKeyEvent(event termbox.Event) int {
switch event.Key {
case termbox.KeyF5:
if err := s.SaveState("quick"); err != nil {
s.Logln("Engine Error: " + err.Error())
} else {
s.Logln("Quick Saved")
}
case termbox.KeyF7:
if err := s.LoadState("quick"); err != nil {
s.Logln("Engine Error: " + err.Error())
} else {
s.Logln("Quick Loaded")
}
case termbox.KeyPgup, termbox.KeyCtrlB:
// Page down the log
wrkLog := s.GetWrappedLog()
if s.LogStartIdx < len(wrkLog)-s.LogDisplayLines {
s.LogStartIdx = s.LogStartIdx + s.LogDisplayLines
}
if s.LogStartIdx > len(wrkLog)-s.LogDisplayLines {
s.LogStartIdx = len(wrkLog) - s.LogDisplayLines
}
case termbox.KeyPgdn, termbox.KeyCtrlF:
// Page up the log
if s.LogStartIdx > 0 {
s.LogStartIdx = s.LogStartIdx - s.LogDisplayLines
}
if s.LogStartIdx < 0 {
s.LogStartIdx = 0
}
default:
if s.Mode == ModeInput {
err := s.Cli.handleEvent(event)
if err != nil {
s.Log(err.Error())
}
}
}
return s.Id()
}
// ╭╼╾╮
// │┼━│
// ├┴┬┤
// ╰──╯
func (s *Screen) DrawScreen() {
s.DrawHeader()
s.DrawLog()
s.DrawInventory()
s.DrawTerminal()
}
func (s *Screen) DrawLog() {
w, h := termbox.Size()
y := 1
logW := w - 2
h = h - 4
s.LogDisplayLines = h - 1
wrkLog := s.GetWrappedLog()
currStart := len(wrkLog) - s.LogDisplayLines - s.LogStartIdx
if currStart < 0 {
currStart = 0
}
statusStart := (len(wrkLog) - s.LogDisplayLines - currStart)
if statusStart < 0 {
statusStart = 0
}
logStatus := fmt.Sprintf("%d/%d (%d:%d)", statusStart, len(wrkLog), s.LogStartIdx, currStart)
_, nY := termboxUtil.DrawStringAtPoint("╭"+logStatus+strings.Repeat("─", (logW-1-(len(logStatus))))+"╮", 1, y, Fg, Bg)
for len(wrkLog) < s.LogDisplayLines {
wrkLog = append([]string{"~"}, wrkLog...)
}
for k := currStart; k < len(wrkLog) && k < currStart+s.LogDisplayLines; k++ {
nY = nY + 1
v := "~"
if k >= 0 {
v = wrkLog[k]
}
if len(v) > logW-4 {
v = v[:logW-4]
} else if len(v) < logW-4 {
v = termboxUtil.AlignTextWithFill(v, logW-4, termboxUtil.AlignLeft, ' ')
}
_, nY = termboxUtil.DrawStringAtPoint(v, 2, nY, Fg, Bg)
termbox.SetCell(1, nY, '│', Fg, Bg)
termbox.SetCell((w - 1), nY, '│', Fg, Bg)
}
termboxUtil.DrawStringAtPoint("├"+strings.Repeat("─", (logW-1))+"┤", 1, nY+1, Fg, Bg)
}
func (s *Screen) DrawInventory() {
w, _ := termbox.Size()
title := "Inventory"
longest := len(title)
for k := range s.Inventory {
if len(s.Inventory[k]) > longest {
longest = len(s.Inventory[k])
}
}
title = "┬" + title + strings.Repeat("─", longest-len(title)) + "╮"
termboxUtil.DrawStringAtPoint(title, w-(longest+2), 1, Fg, Bg)
for k := range s.Inventory {
termboxUtil.DrawStringAtPoint(s.Inventory[k], w-(longest+1), 2+k, Fg, Bg)
termbox.SetCell((w - longest - 2), 2+k, '│', Fg, Bg)
termbox.SetCell((w - 1), 2+k, '│', Fg, Bg)
}
termboxUtil.DrawStringAtPoint("╰"+strings.Repeat("─", len(title)-6)+"┤", w-(longest+2), 2+len(s.Inventory), Fg, Bg)
}
func (s *Screen) DrawTerminal() {
w, h := termbox.Size()
termbox.SetCell(1, h-2, '│', Fg, Bg)
termbox.SetCell((w - 1), h-2, '│', Fg, Bg)
if s.Mode == ModeInput && !s.Cli.Complete {
s.Cli.Draw(2, h-3, Fg, Bg)
} else {
s.DrawStatusLine()
}
termboxUtil.DrawStringAtPoint("╰"+strings.Repeat("─", (w-3))+"╯", 1, h-1, Fg, Bg)
}
func (s *Screen) DrawStatusLine() {
_, h := termbox.Size()
if s.Program.Ptr != s.LastPI {
s.StatusSpinner = (s.StatusSpinner + 1) % len(spinner)
s.LastPI = s.Program.Ptr
}
status := string(spinner[s.StatusSpinner]) + " " + strconv.Itoa(s.LastPI)
for len(status) < 10 {
status = status + " "
}
termboxUtil.DrawStringAtPoint(status+" - "+s.StatusText, 2, h-2, Fg, Bg)
}
func (s *Screen) DrawHeader() {
width, _ := termbox.Size()
spaces := strings.Repeat(" ", ((width-len(s.Title))/2)+1)
termboxUtil.DrawStringAtPoint(fmt.Sprintf("%s%s%s", spaces, s.Title, spaces), 0, 0, Bg, Fg)
}
func (s *Screen) ResizeScreen() {
s.Initialize(nil)
}
func (s *Screen) GetWrappedLog() []string {
w, _ := termbox.Size()
var wrkLog []string
for _, v := range s.TheLog {
var line string
pts := strings.Fields(v)
for k := range pts {
if len(line) == 0 {
line = pts[k]
} else {
if len(line+" "+pts[k]) < w-4 {
line = line + " " + pts[k]
} else {
wrkLog = append(wrkLog, line)
line = pts[k]
}
}
}
wrkLog = append(wrkLog, line)
}
return wrkLog
}
func (s *Screen) Logln(msg string) {
s.TheLog = append(s.TheLog, msg)
}
func (s *Screen) Log(msg string) {
last := s.TheLog[len(s.TheLog)-1]
last = last + msg
s.TheLog[len(s.TheLog)-1] = last
if last == "Command?" {
s.ParseLogForCommands()
}
}
func (s *Screen) ParseLogForCommands() {
// First fine the _previous_ 'Command?'
s.Cli.ClearSuggestions()
for k := len(s.TheLog) - 1; k >= 0; k-- {
if strings.HasPrefix(s.TheLog[k], "== ") {
// Room start
break
}
if strings.HasPrefix(s.TheLog[k], "- ") {
val := strings.TrimPrefix(s.TheLog[k], "- ")
switch val {
case "north", "east", "south", "west":
s.Cli.AddSuggestion(val)
default:
var have bool
for _, v := range s.Inventory {
if v == val {
have = true
break
}
}
if !have {
s.Cli.AddSuggestion("take " + val)
}
}
}
}
for _, v := range s.Inventory {
s.Cli.AddSuggestions([]string{"drop " + v})
}
}
func (s *Screen) IsEngineCommand(inp string) bool {
return inp == "/quit" || strings.HasPrefix(inp, "/save") || strings.HasPrefix(inp, "/load")
}
func (s *Screen) ProcessCommand(inp string) error {
if inp == "/quit" {
s.id = -1
} else if strings.HasPrefix(inp, "/save ") {
pts := strings.Split(inp, " ")
if len(pts) == 2 {
return s.SaveState(pts[1])
}
return errors.New("No filename given")
} else if strings.HasPrefix(inp, "/load ") {
pts := strings.Split(inp, " ")
if len(pts) == 2 {
return s.LoadState(pts[1])
}
return errors.New("No filename given")
}
return nil
}
func (s *Screen) SaveState(slot string) error {
// Save the engine state first
f, err := os.OpenFile("saveslot-"+slot+".sav", os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.Encode(s)
return nil
}
func (s *Screen) LoadState(slot string) error {
hold, err := ioutil.ReadFile("saveslot-" + slot + ".sav")
if err != nil {
return err
}
json.NewDecoder(bytes.NewBuffer(hold)).Decode(&s)
return nil
}