405 lines
9.4 KiB
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
|
||
|
}
|