package game import "fmt" // The screen layer replaces curses. Game code draws into Window cell // buffers (stdscr and the hw scratch window); a Terminal implementation // blits them to a real device. Tests run with a scripted Terminal (or none // at all), which is also how the "screen is a data structure" idiom — // C code reading back what it drew with inch() — stays intact headlessly. // Terminal is the physical device: a tcell screen in the real game, a // script in tests. type Terminal interface { // Render blits the window to the device. Render(w *Window) // ReadChar blocks for the next key, translated to Rogue's input bytes // (arrows become hjkl, control keys their C0 codes). ReadChar() byte } // cell is one screen position. type cell struct { ch byte standout bool } // Window is an in-memory curses window: a cell grid with a cursor and a // standout attribute. type Window struct { rows, cols int cells []cell cy, cx int standout bool } // NewWindow returns a cleared window. func NewWindow(rows, cols int) *Window { w := &Window{rows: rows, cols: cols, cells: make([]cell, rows*cols)} w.Clear() return w } func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] } // Move positions the cursor (curses move/wmove). func (w *Window) Move(y, x int) { w.cy, w.cx = y, x } // GetYX reports the cursor position (curses getyx). func (w *Window) GetYX() (y, x int) { return w.cy, w.cx } // AddCh writes a character at the cursor and advances it (curses addch). func (w *Window) AddCh(ch byte) { if ch == '\n' { w.cy, w.cx = w.cy+1, 0 return } if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols { return } *w.at(w.cy, w.cx) = cell{ch: ch, standout: w.standout} if w.cx++; w.cx >= w.cols { w.cx = 0 if w.cy < w.rows-1 { w.cy++ } } } // AddStr writes a string at the cursor (curses addstr). func (w *Window) AddStr(s string) { for i := 0; i < len(s); i++ { w.AddCh(s[i]) } } // MvAddCh moves then writes (curses mvaddch). func (w *Window) MvAddCh(y, x int, ch byte) { w.Move(y, x) w.AddCh(ch) } // MvAddStr moves then writes (curses mvaddstr). func (w *Window) MvAddStr(y, x int, s string) { w.Move(y, x) w.AddStr(s) } // Printw writes formatted text at the cursor (curses printw). func (w *Window) Printw(format string, a ...any) { w.AddStr(fmt.Sprintf(format, a...)) } // MvPrintw moves then writes formatted text (curses mvprintw). func (w *Window) MvPrintw(y, x int, format string, a ...any) { w.Move(y, x) w.Printw(format, a...) } // Inch returns the character under the cursor (curses inch, sans // attributes — the C code always strips them with CCHAR). func (w *Window) Inch() byte { if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols { return ' ' } return w.at(w.cy, w.cx).ch } // MvInch moves then reads (curses mvinch). func (w *Window) MvInch(y, x int) byte { w.Move(y, x) return w.Inch() } // Standout sets or clears the standout attribute for subsequent writes // (curses standout/standend). func (w *Window) Standout(on bool) { w.standout = on } // Clear blanks the window and homes the cursor (curses clear/wclear). func (w *Window) Clear() { for i := range w.cells { w.cells[i] = cell{ch: ' '} } w.cy, w.cx = 0, 0 } // Clrtoeol blanks from the cursor to the end of the line (curses clrtoeol). func (w *Window) Clrtoeol() { if w.cy < 0 || w.cy >= w.rows { return } for x := w.cx; x < w.cols; x++ { *w.at(w.cy, x) = cell{ch: ' '} } } // CopyFrom copies another window's contents (curses overwrite). func (w *Window) CopyFrom(src *Window) { copy(w.cells, src.cells) } // Size reports the window dimensions. func (w *Window) Size() (rows, cols int) { return w.rows, w.cols } // CellAt reports the character and standout attribute at a position; used // by Terminal implementations to render the window. func (w *Window) CellAt(y, x int) (ch byte, standout bool) { c := w.at(y, x) return c.ch, c.standout } // Contents dumps the window characters row-major (the save file keeps the // visible map, as the C game saved the curses screen). func (w *Window) Contents() []byte { out := make([]byte, len(w.cells)) for i, c := range w.cells { out[i] = c.ch } return out } // SetContents restores a Contents dump. func (w *Window) SetContents(data []byte) { for i := range w.cells { if i < len(data) { w.cells[i] = cell{ch: data[i]} } } } // Line returns row y as a trimmed string; used by tests and the death/ // victory screens. func (w *Window) Line(y int) string { buf := make([]byte, w.cols) for x := 0; x < w.cols; x++ { buf[x] = w.at(y, x).ch } return string(buf) } // Screen bundles the two windows the game draws on with the device that // shows them. type Screen struct { term Terminal Std *Window // stdscr: the dungeon view Hw *Window // hw: the scratch window for overlays } // NewScreen builds the standard 24x80 game screen. func NewScreen(term Terminal) *Screen { return &Screen{ term: term, Std: NewWindow(NumLines, NumCols), Hw: NewWindow(NumLines, NumCols), } } // Refresh pushes stdscr to the device (curses refresh). func (s *Screen) Refresh() { if s.term != nil { s.term.Render(s.Std) } } // RefreshWin pushes an arbitrary window to the device (curses wrefresh). func (s *Screen) RefreshWin(w *Window) { if s.term != nil { s.term.Render(w) } } // Thin RogueGame wrappers so ported bodies keep their curses shape. func (g *RogueGame) move(y, x int) { g.scr.Std.Move(y, x) } func (g *RogueGame) addch(ch byte) { g.scr.Std.AddCh(ch) } func (g *RogueGame) addstr(s string) { g.scr.Std.AddStr(s) } func (g *RogueGame) mvaddch(y, x int, c byte) { g.scr.Std.MvAddCh(y, x, c) } func (g *RogueGame) mvaddstr(y, x int, s string) { g.scr.Std.MvAddStr(y, x, s) } func (g *RogueGame) printw(f string, a ...any) { g.scr.Std.Printw(f, a...) } func (g *RogueGame) inch() byte { return g.scr.Std.Inch() } func (g *RogueGame) mvinch(y, x int) byte { return g.scr.Std.MvInch(y, x) } func (g *RogueGame) standout() { g.scr.Std.Standout(true) } func (g *RogueGame) standend() { g.scr.Std.Standout(false) } func (g *RogueGame) clear() { g.scr.Std.Clear() } func (g *RogueGame) clrtoeol() { g.scr.Std.Clrtoeol() } func (g *RogueGame) refresh() { g.scr.Refresh() }