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 ' 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 }