go: port command loop, save/restore, tcell terminal, and the binary
- command.c in full: the command dispatcher with repeat counts, run prefixes, ctrl-run door-stop mode, fight-to-death targeting, and all wizard debug commands; search, help, identify, d_level/u_level, call, current - main.c completed: Run()/playit() with the C double ROGUEOPTS parse; exit()-anywhere becomes a gameEnd panic recovered in Run - save.c + state.c: gob SaveState snapshot with explicit pointer-to- index fixups for equipment, monster rooms, and chase targets (the rs_fix_thing role); save file deleted on restore as in C; SIGHUP/ SIGTERM autosave hook - wizard.c completed (create_obj, show_map), passages.c add_pass - term/: tcell/v2 Terminal — the one third-party dependency — replacing curses and mdport's 900-line key decoder; shell escape via suspend - cmd/rogue: flags -s/-d, SEED (wizard), ROGUEOPTS, ROGUE_WIZARD Tests: full scripted sessions through Run (quit, descend stairs, 3200-move crash sweep, save-command round trip). Notable fixes found by tests: gob silently drops embedded fields of unexported types, and --More-- prompts swallow space-free input scripts.
This commit is contained in:
135
term/tcell.go
Normal file
135
term/tcell.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Package term provides the tcell-backed Terminal for the Rogue port. It
|
||||
// replaces curses and the 900-line escape-sequence decoder in mdport.c:
|
||||
// tcell delivers decoded key events, which are translated here to the
|
||||
// single-byte command codes the game understands.
|
||||
package term
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
||||
"sneak.berlin/go/rogue/game"
|
||||
)
|
||||
|
||||
// Tcell renders game Windows on a tcell screen and turns key events into
|
||||
// Rogue input bytes.
|
||||
type Tcell struct {
|
||||
screen tcell.Screen
|
||||
last *game.Window // last rendered window, for resize redraws
|
||||
}
|
||||
|
||||
// New initializes the terminal. The screen must be at least 80x24, as the
|
||||
// C game required.
|
||||
func New() (*Tcell, error) {
|
||||
s, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, h := s.Size()
|
||||
if h < game.NumLines || w < game.NumCols {
|
||||
s.Fini()
|
||||
return nil, fmt.Errorf("sorry, the screen must be at least %dx%d",
|
||||
game.NumLines, game.NumCols)
|
||||
}
|
||||
s.HideCursor()
|
||||
return &Tcell{screen: s}, nil
|
||||
}
|
||||
|
||||
// Fini restores the terminal.
|
||||
func (t *Tcell) Fini() {
|
||||
t.screen.Fini()
|
||||
}
|
||||
|
||||
// Render blits a game window to the terminal (curses refresh).
|
||||
func (t *Tcell) Render(w *game.Window) {
|
||||
t.last = w
|
||||
rows, cols := w.Size()
|
||||
for y := 0; y < rows; y++ {
|
||||
for x := 0; x < cols; x++ {
|
||||
ch, standout := w.CellAt(y, x)
|
||||
style := tcell.StyleDefault
|
||||
if standout {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
t.screen.SetContent(x, y, rune(ch), nil, style)
|
||||
}
|
||||
}
|
||||
t.screen.Show()
|
||||
}
|
||||
|
||||
// ReadChar blocks for the next key, translated to the byte codes the C
|
||||
// game reads: arrows become hjkl, control keys their C0 codes.
|
||||
func (t *Tcell) ReadChar() byte {
|
||||
for {
|
||||
ev := t.screen.PollEvent()
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
if t.last != nil {
|
||||
t.Render(t.last)
|
||||
}
|
||||
case *tcell.EventKey:
|
||||
switch ev.Key() {
|
||||
case tcell.KeyUp:
|
||||
return 'k'
|
||||
case tcell.KeyDown:
|
||||
return 'j'
|
||||
case tcell.KeyLeft:
|
||||
return 'h'
|
||||
case tcell.KeyRight:
|
||||
return 'l'
|
||||
case tcell.KeyHome:
|
||||
return 'y'
|
||||
case tcell.KeyPgUp:
|
||||
return 'u'
|
||||
case tcell.KeyEnd:
|
||||
return 'b'
|
||||
case tcell.KeyPgDn:
|
||||
return 'n'
|
||||
case tcell.KeyEnter:
|
||||
return '\n'
|
||||
case tcell.KeyEscape:
|
||||
return game.Escape
|
||||
case tcell.KeyBackspace, tcell.KeyBackspace2:
|
||||
return 8
|
||||
case tcell.KeyDelete:
|
||||
return 0x7f
|
||||
case tcell.KeyTab:
|
||||
return '\t'
|
||||
case tcell.KeyCtrlC:
|
||||
return 3
|
||||
default:
|
||||
if ev.Key() >= tcell.KeyCtrlA && ev.Key() <= tcell.KeyCtrlZ {
|
||||
return byte(ev.Key())
|
||||
}
|
||||
if r := ev.Rune(); r > 0 && r < 0x80 {
|
||||
return byte(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ShellEscape suspends the screen and runs the user's shell (main.c
|
||||
// shell + md_shellescape).
|
||||
func (t *Tcell) ShellEscape() {
|
||||
if err := t.screen.Suspend(); err != nil {
|
||||
return
|
||||
}
|
||||
shell := os.Getenv("SHELL")
|
||||
if shell == "" {
|
||||
shell = "/bin/sh"
|
||||
}
|
||||
fmt.Println("[Entering shell; exit to return to the game]")
|
||||
cmd := exec.Command(shell)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Run()
|
||||
t.screen.Resume()
|
||||
}
|
||||
Reference in New Issue
Block a user