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:
101
cmd/rogue/main.go
Normal file
101
cmd/rogue/main.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Command rogue is the Go port of Rogue 5.4.4: Exploring the Dungeons of
|
||||||
|
// Doom. It is a faithful function-by-function port of the classic C game;
|
||||||
|
// see ARCHITECTURE.md at the repository root.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"sneak.berlin/go/rogue/game"
|
||||||
|
"sneak.berlin/go/rogue/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
scores := flag.Bool("s", false, "print the scoreboard and exit")
|
||||||
|
deathDemo := flag.Bool("d", false, "die a random death (demo)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
|
||||||
|
// get options from environment (main.c)
|
||||||
|
rogueOpts := os.Getenv("ROGUEOPTS")
|
||||||
|
name := ""
|
||||||
|
if u, err := user.Current(); err == nil {
|
||||||
|
name = u.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
wizard := os.Getenv("ROGUE_WIZARD") != ""
|
||||||
|
// dungeon number: SEED for reproducible dungeons (wizard mode in C),
|
||||||
|
// else time+pid
|
||||||
|
var seed int32
|
||||||
|
if env := os.Getenv("SEED"); env != "" && wizard {
|
||||||
|
n, _ := strconv.Atoi(env)
|
||||||
|
seed = int32(n)
|
||||||
|
} else {
|
||||||
|
seed = int32(time.Now().Unix()) + int32(os.Getpid())
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := game.Config{
|
||||||
|
Seed: seed,
|
||||||
|
Name: name,
|
||||||
|
RogueOpts: rogueOpts,
|
||||||
|
Home: home,
|
||||||
|
ScorePath: home + "/.rogue.scores",
|
||||||
|
Wizard: wizard,
|
||||||
|
}
|
||||||
|
|
||||||
|
if *scores {
|
||||||
|
g := game.NewGame(cfg)
|
||||||
|
g.ShowScores()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := term.New()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer t.Fini()
|
||||||
|
cfg.Term = t
|
||||||
|
|
||||||
|
var g *game.RogueGame
|
||||||
|
if args := flag.Args(); len(args) == 1 && !*deathDemo {
|
||||||
|
// restore a saved game
|
||||||
|
g, err = game.Restore(args[0], cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fini()
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g = game.NewGame(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *deathDemo {
|
||||||
|
g.DeathDemo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SIGHUP/SIGTERM autosave (save.c auto_save)
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGHUP, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sig
|
||||||
|
g.AutoSave()
|
||||||
|
t.Fini()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := g.Run(); err != nil {
|
||||||
|
t.Fini()
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
769
game/command.go
Normal file
769
game/command.go
Normal file
@@ -0,0 +1,769 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
// command.c — read and execute the user commands.
|
||||||
|
|
||||||
|
// command processes one player command, with its BEFORE/AFTER daemon
|
||||||
|
// bracketing (command.c command).
|
||||||
|
func (g *RogueGame) command() {
|
||||||
|
p := &g.Player
|
||||||
|
ntimes := 1 // number of player moves
|
||||||
|
if p.On(IsHaste) {
|
||||||
|
ntimes++
|
||||||
|
}
|
||||||
|
// Let the daemons start up
|
||||||
|
g.DoDaemons(Before)
|
||||||
|
g.DoFuses(Before)
|
||||||
|
for ; ntimes > 0; ntimes-- {
|
||||||
|
g.Again = false
|
||||||
|
if g.HasHit {
|
||||||
|
g.endmsg()
|
||||||
|
g.HasHit = false
|
||||||
|
}
|
||||||
|
// these are illegal things for the player to be, so if any are
|
||||||
|
// set, someone's been poking in memory
|
||||||
|
if p.On(IsSlow | IsGreed | IsInvis | IsRegen | IsTarget) {
|
||||||
|
panic("player flags corrupted")
|
||||||
|
}
|
||||||
|
|
||||||
|
g.look(true)
|
||||||
|
if !g.Running {
|
||||||
|
g.DoorStop = false
|
||||||
|
}
|
||||||
|
g.status()
|
||||||
|
g.LastScore = p.Purse
|
||||||
|
g.move(p.Pos.Y, p.Pos.X)
|
||||||
|
if !((g.Running || g.Count != 0) && g.Options.Jump) {
|
||||||
|
g.refresh() // draw screen
|
||||||
|
}
|
||||||
|
g.Take = 0
|
||||||
|
g.After = true
|
||||||
|
// Read command or continue run
|
||||||
|
if g.Wizard {
|
||||||
|
g.NoScore = true
|
||||||
|
}
|
||||||
|
var ch byte
|
||||||
|
if g.NoCommand == 0 {
|
||||||
|
if g.Running || g.ToDeath {
|
||||||
|
ch = g.RunCh
|
||||||
|
} else if g.Count != 0 {
|
||||||
|
ch = g.countCh
|
||||||
|
} else {
|
||||||
|
ch = g.readchar()
|
||||||
|
g.MoveOn = false
|
||||||
|
if g.Msgs.Mpos != 0 { // erase message if its there
|
||||||
|
g.msg("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ch = '.'
|
||||||
|
}
|
||||||
|
if g.NoCommand != 0 {
|
||||||
|
if g.NoCommand--; g.NoCommand == 0 {
|
||||||
|
p.Flags.Set(IsRun)
|
||||||
|
g.msg("you can move again")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// check for prefixes
|
||||||
|
g.newCount = false
|
||||||
|
if isDigit(ch) {
|
||||||
|
g.Count = 0
|
||||||
|
g.newCount = true
|
||||||
|
for isDigit(ch) {
|
||||||
|
g.Count = g.Count*10 + int(ch-'0')
|
||||||
|
if g.Count > 255 {
|
||||||
|
g.Count = 255
|
||||||
|
}
|
||||||
|
ch = g.readchar()
|
||||||
|
}
|
||||||
|
g.countCh = ch
|
||||||
|
// turn off count for commands which don't make sense
|
||||||
|
// to repeat
|
||||||
|
switch ch {
|
||||||
|
case CTRL('B'), CTRL('H'), CTRL('J'), CTRL('K'),
|
||||||
|
CTRL('L'), CTRL('N'), CTRL('U'), CTRL('Y'),
|
||||||
|
'.', 'a', 'b', 'h', 'j', 'k', 'l', 'm', 'n', 'q',
|
||||||
|
'r', 's', 't', 'u', 'y', 'z', 'B', 'C', 'H', 'I',
|
||||||
|
'J', 'K', 'L', 'N', 'U', 'Y',
|
||||||
|
CTRL('D'), CTRL('A'):
|
||||||
|
default:
|
||||||
|
g.Count = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// execute a command
|
||||||
|
if g.Count != 0 && !g.Running {
|
||||||
|
g.Count--
|
||||||
|
}
|
||||||
|
if ch != 'a' && ch != Escape &&
|
||||||
|
!(g.Running || g.Count != 0 || g.ToDeath) {
|
||||||
|
g.LLastComm = g.LastComm
|
||||||
|
g.LLastDir = g.LastDir
|
||||||
|
g.LLastPick = g.LastPick
|
||||||
|
g.LastComm = ch
|
||||||
|
g.LastDir = 0
|
||||||
|
g.LastPick = nil
|
||||||
|
}
|
||||||
|
g.dispatch(ch)
|
||||||
|
// turn off flags if no longer needed
|
||||||
|
if !g.Running {
|
||||||
|
g.DoorStop = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If he ran into something to take, let him pick it up.
|
||||||
|
if g.Take != 0 {
|
||||||
|
g.pickUp(g.Take)
|
||||||
|
}
|
||||||
|
if !g.Running {
|
||||||
|
g.DoorStop = false
|
||||||
|
}
|
||||||
|
if !g.After {
|
||||||
|
ntimes++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.DoDaemons(After)
|
||||||
|
g.DoFuses(After)
|
||||||
|
if p.IsRing(Left, RSearch) {
|
||||||
|
g.search()
|
||||||
|
} else if p.IsRing(Left, RTeleport) && g.rnd(50) == 0 {
|
||||||
|
g.teleport()
|
||||||
|
}
|
||||||
|
if p.IsRing(Right, RSearch) {
|
||||||
|
g.search()
|
||||||
|
} else if p.IsRing(Right, RTeleport) && g.rnd(50) == 0 {
|
||||||
|
g.teleport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch runs one command character (the big switch in command.c,
|
||||||
|
// including its `goto over` re-dispatch).
|
||||||
|
func (g *RogueGame) dispatch(ch byte) {
|
||||||
|
p := &g.Player
|
||||||
|
over:
|
||||||
|
switch ch {
|
||||||
|
case ',':
|
||||||
|
var found *Object
|
||||||
|
for _, obj := range g.Level.Objects {
|
||||||
|
if obj.Pos.Y == p.Pos.Y && obj.Pos.X == p.Pos.X {
|
||||||
|
found = obj
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found != nil {
|
||||||
|
if !g.levitCheck() {
|
||||||
|
g.pickUp(found.Type)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !g.Options.Terse {
|
||||||
|
g.addmsg("there is ")
|
||||||
|
}
|
||||||
|
g.addmsg("nothing here")
|
||||||
|
if !g.Options.Terse {
|
||||||
|
g.addmsg(" to pick up")
|
||||||
|
}
|
||||||
|
g.endmsg()
|
||||||
|
}
|
||||||
|
case '!':
|
||||||
|
g.shell()
|
||||||
|
case 'h':
|
||||||
|
g.doMove(0, -1)
|
||||||
|
case 'j':
|
||||||
|
g.doMove(1, 0)
|
||||||
|
case 'k':
|
||||||
|
g.doMove(-1, 0)
|
||||||
|
case 'l':
|
||||||
|
g.doMove(0, 1)
|
||||||
|
case 'y':
|
||||||
|
g.doMove(-1, -1)
|
||||||
|
case 'u':
|
||||||
|
g.doMove(-1, 1)
|
||||||
|
case 'b':
|
||||||
|
g.doMove(1, -1)
|
||||||
|
case 'n':
|
||||||
|
g.doMove(1, 1)
|
||||||
|
case 'H':
|
||||||
|
g.doRun('h')
|
||||||
|
case 'J':
|
||||||
|
g.doRun('j')
|
||||||
|
case 'K':
|
||||||
|
g.doRun('k')
|
||||||
|
case 'L':
|
||||||
|
g.doRun('l')
|
||||||
|
case 'Y':
|
||||||
|
g.doRun('y')
|
||||||
|
case 'U':
|
||||||
|
g.doRun('u')
|
||||||
|
case 'B':
|
||||||
|
g.doRun('b')
|
||||||
|
case 'N':
|
||||||
|
g.doRun('n')
|
||||||
|
case CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'),
|
||||||
|
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'):
|
||||||
|
if !p.On(IsBlind) {
|
||||||
|
g.DoorStop = true
|
||||||
|
g.Firstmove = true
|
||||||
|
}
|
||||||
|
if g.Count != 0 && !g.newCount {
|
||||||
|
ch = g.direction
|
||||||
|
} else {
|
||||||
|
ch += 'A' - CTRL('A')
|
||||||
|
g.direction = ch
|
||||||
|
}
|
||||||
|
goto over
|
||||||
|
case 'F', 'f':
|
||||||
|
if ch == 'F' {
|
||||||
|
g.Kamikaze = true
|
||||||
|
}
|
||||||
|
if !g.getDir() {
|
||||||
|
g.After = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
g.Delta.Y += p.Pos.Y
|
||||||
|
g.Delta.X += p.Pos.X
|
||||||
|
mp := g.Level.MonsterAt(g.Delta.Y, g.Delta.X)
|
||||||
|
if mp == nil || (!g.seeMonst(mp) && !p.On(SeeMonst)) {
|
||||||
|
if !g.Options.Terse {
|
||||||
|
g.addmsg("I see ")
|
||||||
|
}
|
||||||
|
g.msg("no monster there")
|
||||||
|
g.After = false
|
||||||
|
} else if g.diagOk(p.Pos, g.Delta) {
|
||||||
|
g.ToDeath = true
|
||||||
|
g.MaxHit = 0
|
||||||
|
mp.Flags.Set(IsTarget)
|
||||||
|
g.RunCh = g.DirCh
|
||||||
|
ch = g.DirCh
|
||||||
|
goto over
|
||||||
|
}
|
||||||
|
case 't':
|
||||||
|
if !g.getDir() {
|
||||||
|
g.After = false
|
||||||
|
} else {
|
||||||
|
g.missile(g.Delta.Y, g.Delta.X)
|
||||||
|
}
|
||||||
|
case 'a':
|
||||||
|
if g.LastComm == 0 {
|
||||||
|
g.msg("you haven't typed a command yet")
|
||||||
|
g.After = false
|
||||||
|
} else {
|
||||||
|
ch = g.LastComm
|
||||||
|
g.Again = true
|
||||||
|
goto over
|
||||||
|
}
|
||||||
|
case 'q':
|
||||||
|
g.quaff()
|
||||||
|
case 'Q':
|
||||||
|
g.After = false
|
||||||
|
g.QComm = true
|
||||||
|
g.quit(0)
|
||||||
|
g.QComm = false
|
||||||
|
case 'i':
|
||||||
|
g.After = false
|
||||||
|
g.inventory(p.Pack, 0)
|
||||||
|
case 'I':
|
||||||
|
g.After = false
|
||||||
|
g.pickyInven()
|
||||||
|
case 'd':
|
||||||
|
g.dropIt()
|
||||||
|
case 'r':
|
||||||
|
g.readScroll()
|
||||||
|
case 'e':
|
||||||
|
g.eat()
|
||||||
|
case 'w':
|
||||||
|
g.wield()
|
||||||
|
case 'W':
|
||||||
|
g.wear()
|
||||||
|
case 'T':
|
||||||
|
g.takeOff()
|
||||||
|
case 'P':
|
||||||
|
g.ringOn()
|
||||||
|
case 'R':
|
||||||
|
g.ringOff()
|
||||||
|
case 'o':
|
||||||
|
g.option()
|
||||||
|
g.After = false
|
||||||
|
case 'c':
|
||||||
|
g.call()
|
||||||
|
g.After = false
|
||||||
|
case '>':
|
||||||
|
g.After = false
|
||||||
|
g.dLevel()
|
||||||
|
case '<':
|
||||||
|
g.After = false
|
||||||
|
g.uLevel()
|
||||||
|
case '?':
|
||||||
|
g.After = false
|
||||||
|
g.help()
|
||||||
|
case '/':
|
||||||
|
g.After = false
|
||||||
|
g.identify()
|
||||||
|
case 's':
|
||||||
|
g.search()
|
||||||
|
case 'z':
|
||||||
|
if g.getDir() {
|
||||||
|
g.doZap()
|
||||||
|
} else {
|
||||||
|
g.After = false
|
||||||
|
}
|
||||||
|
case 'D':
|
||||||
|
g.After = false
|
||||||
|
g.discovered()
|
||||||
|
case CTRL('P'):
|
||||||
|
g.After = false
|
||||||
|
g.msg("%s", g.Msgs.Huh)
|
||||||
|
case CTRL('R'):
|
||||||
|
g.After = false
|
||||||
|
g.refresh()
|
||||||
|
case 'v':
|
||||||
|
g.After = false
|
||||||
|
g.msg("version %s. (mctesq was here)", Release)
|
||||||
|
case 'S':
|
||||||
|
g.After = false
|
||||||
|
g.saveGame()
|
||||||
|
case '.':
|
||||||
|
// rest command
|
||||||
|
case ' ':
|
||||||
|
g.After = false // "legal" illegal command
|
||||||
|
case '^':
|
||||||
|
g.After = false
|
||||||
|
if g.getDir() {
|
||||||
|
g.Delta.Y += p.Pos.Y
|
||||||
|
g.Delta.X += p.Pos.X
|
||||||
|
fp := g.Level.FlagsAt(g.Delta.Y, g.Delta.X)
|
||||||
|
if !g.Options.Terse {
|
||||||
|
g.addmsg("You have found ")
|
||||||
|
}
|
||||||
|
if g.Level.Char(g.Delta.Y, g.Delta.X) != Trap {
|
||||||
|
g.msg("no trap there")
|
||||||
|
} else if p.On(IsHalu) {
|
||||||
|
g.msg("%s", trName[g.rnd(NTraps)])
|
||||||
|
} else {
|
||||||
|
g.msg("%s", trName[*fp&FTMask])
|
||||||
|
fp.Set(FSeen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Escape: // escape
|
||||||
|
g.DoorStop = false
|
||||||
|
g.Count = 0
|
||||||
|
g.After = false
|
||||||
|
g.Again = false
|
||||||
|
case 'm':
|
||||||
|
g.MoveOn = true
|
||||||
|
if !g.getDir() {
|
||||||
|
g.After = false
|
||||||
|
} else {
|
||||||
|
ch = g.DirCh
|
||||||
|
g.countCh = g.DirCh
|
||||||
|
goto over
|
||||||
|
}
|
||||||
|
case ')':
|
||||||
|
g.current(p.CurWeapon, "wielding", "")
|
||||||
|
case ']':
|
||||||
|
g.current(p.CurArmor, "wearing", "")
|
||||||
|
case '=':
|
||||||
|
g.current(p.CurRing[Left], "wearing",
|
||||||
|
g.chooseTerse("(L)", "on left hand"))
|
||||||
|
g.current(p.CurRing[Right], "wearing",
|
||||||
|
g.chooseTerse("(R)", "on right hand"))
|
||||||
|
case '@':
|
||||||
|
g.StatMsg = true
|
||||||
|
g.status()
|
||||||
|
g.StatMsg = false
|
||||||
|
g.After = false
|
||||||
|
default:
|
||||||
|
g.After = false
|
||||||
|
if g.Wizard {
|
||||||
|
g.wizardCommand(ch)
|
||||||
|
} else {
|
||||||
|
g.illcom(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wizardCommand handles the MASTER debug commands (command.c).
|
||||||
|
func (g *RogueGame) wizardCommand(ch byte) {
|
||||||
|
p := &g.Player
|
||||||
|
switch ch {
|
||||||
|
case '|':
|
||||||
|
g.msg("@ %d,%d", p.Pos.Y, p.Pos.X)
|
||||||
|
case 'C':
|
||||||
|
g.createObj()
|
||||||
|
case '$':
|
||||||
|
g.msg("inpack = %d", p.Inpack)
|
||||||
|
case CTRL('G'):
|
||||||
|
g.inventory(g.Level.Objects, 0)
|
||||||
|
case CTRL('W'):
|
||||||
|
g.whatis(false, 0)
|
||||||
|
case CTRL('D'):
|
||||||
|
g.Depth++
|
||||||
|
g.NewLevel()
|
||||||
|
case CTRL('A'):
|
||||||
|
g.Depth--
|
||||||
|
g.NewLevel()
|
||||||
|
case CTRL('F'):
|
||||||
|
g.showMap()
|
||||||
|
case CTRL('T'):
|
||||||
|
g.teleport()
|
||||||
|
case CTRL('E'):
|
||||||
|
g.msg("food left: %d", p.FoodLeft)
|
||||||
|
case CTRL('C'):
|
||||||
|
g.addPass()
|
||||||
|
case CTRL('X'):
|
||||||
|
g.turnSee(p.On(SeeMonst))
|
||||||
|
case CTRL('~'):
|
||||||
|
if item := g.getItem("charge", int(Stick)); item != nil {
|
||||||
|
item.SetCharges(10000)
|
||||||
|
}
|
||||||
|
case CTRL('I'):
|
||||||
|
for range 9 {
|
||||||
|
g.raiseLevel()
|
||||||
|
}
|
||||||
|
// Give him a sword (+1,+1)
|
||||||
|
obj := newObject()
|
||||||
|
g.initWeapon(obj, TwoSword)
|
||||||
|
obj.HPlus = 1
|
||||||
|
obj.DPlus = 1
|
||||||
|
g.addPack(obj, true)
|
||||||
|
p.CurWeapon = obj
|
||||||
|
// And his suit of armor
|
||||||
|
obj = newObject()
|
||||||
|
obj.Type = Armor
|
||||||
|
obj.Which = PlateMail
|
||||||
|
obj.Arm = -5
|
||||||
|
obj.Flags.Set(IsKnow)
|
||||||
|
obj.Count = 1
|
||||||
|
p.CurArmor = obj
|
||||||
|
g.addPack(obj, true)
|
||||||
|
default:
|
||||||
|
g.illcom(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// illcom reports an illegal command (command.c illcom).
|
||||||
|
func (g *RogueGame) illcom(ch byte) {
|
||||||
|
g.Msgs.SaveMsg = false
|
||||||
|
g.Count = 0
|
||||||
|
g.msg("illegal command '%s'", unctrl(ch))
|
||||||
|
g.Msgs.SaveMsg = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// search gropes about to find hidden things (command.c search).
|
||||||
|
func (g *RogueGame) search() {
|
||||||
|
p := &g.Player
|
||||||
|
ey := p.Pos.Y + 1
|
||||||
|
ex := p.Pos.X + 1
|
||||||
|
probinc := 0
|
||||||
|
if p.On(IsHalu) {
|
||||||
|
probinc = 3
|
||||||
|
}
|
||||||
|
if p.On(IsBlind) {
|
||||||
|
probinc += 2
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for y := p.Pos.Y - 1; y <= ey; y++ {
|
||||||
|
for x := p.Pos.X - 1; x <= ex; x++ {
|
||||||
|
if y == p.Pos.Y && x == p.Pos.X {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fp := g.Level.FlagsAt(y, x)
|
||||||
|
if fp.Has(FReal) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
foundone := false
|
||||||
|
switch g.Level.Char(y, x) {
|
||||||
|
case '|', '-':
|
||||||
|
if g.rnd(5+probinc) != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
g.Level.SetChar(y, x, Door)
|
||||||
|
g.msg("a secret door")
|
||||||
|
foundone = true
|
||||||
|
case Floor:
|
||||||
|
if g.rnd(2+probinc) != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
g.Level.SetChar(y, x, Trap)
|
||||||
|
if !g.Options.Terse {
|
||||||
|
g.addmsg("you found ")
|
||||||
|
}
|
||||||
|
if p.On(IsHalu) {
|
||||||
|
g.msg("%s", trName[g.rnd(NTraps)])
|
||||||
|
} else {
|
||||||
|
g.msg("%s", trName[*fp&FTMask])
|
||||||
|
fp.Set(FSeen)
|
||||||
|
}
|
||||||
|
foundone = true
|
||||||
|
case ' ':
|
||||||
|
if g.rnd(3+probinc) != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
g.Level.SetChar(y, x, Passage)
|
||||||
|
foundone = true
|
||||||
|
}
|
||||||
|
if foundone {
|
||||||
|
found = true
|
||||||
|
fp.Set(FReal)
|
||||||
|
g.Count = 0
|
||||||
|
g.Running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
g.look(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// help gives single character help, or the whole mess if he wants it
|
||||||
|
// (command.c help).
|
||||||
|
func (g *RogueGame) help() {
|
||||||
|
g.msg("character you want help for (* for all): ")
|
||||||
|
helpch := g.readchar()
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
// If it's not a *, print the right help string or an error if he
|
||||||
|
// typed a funny character.
|
||||||
|
if helpch != '*' {
|
||||||
|
g.move(0, 0)
|
||||||
|
for _, strp := range helpStr {
|
||||||
|
if strp.Ch == helpch {
|
||||||
|
g.Msgs.LowerMsg = true
|
||||||
|
g.msg("%s%s", unctrl(strp.Ch), strp.Desc)
|
||||||
|
g.Msgs.LowerMsg = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.msg("unknown character '%s'", unctrl(helpch))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Here we print help for everything, then wait before we return to
|
||||||
|
// command mode.
|
||||||
|
numprint := 0
|
||||||
|
for _, strp := range helpStr {
|
||||||
|
if strp.Print {
|
||||||
|
numprint++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if numprint&1 == 1 { // round odd numbers up
|
||||||
|
numprint++
|
||||||
|
}
|
||||||
|
numprint /= 2
|
||||||
|
if numprint > NumLines-1 {
|
||||||
|
numprint = NumLines - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
hw := g.scr.Hw
|
||||||
|
hw.Clear()
|
||||||
|
cnt := 0
|
||||||
|
for _, strp := range helpStr {
|
||||||
|
if !strp.Print {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x := 0
|
||||||
|
if cnt >= numprint {
|
||||||
|
x = NumCols / 2
|
||||||
|
}
|
||||||
|
hw.Move(cnt%numprint, x)
|
||||||
|
if strp.Ch != 0 {
|
||||||
|
hw.AddStr(unctrl(strp.Ch))
|
||||||
|
}
|
||||||
|
hw.AddStr(strp.Desc)
|
||||||
|
if cnt++; cnt >= numprint*2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hw.MvAddStr(NumLines-1, 0, "--Press space to continue--")
|
||||||
|
g.scr.RefreshWin(hw)
|
||||||
|
g.waitFor(' ')
|
||||||
|
g.msg("")
|
||||||
|
g.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// identList is command.c's static ident_list.
|
||||||
|
var identList = []helpEntry{
|
||||||
|
{'|', "wall of a room", false},
|
||||||
|
{'-', "wall of a room", false},
|
||||||
|
{Gold, "gold", false},
|
||||||
|
{Stairs, "a staircase", false},
|
||||||
|
{Door, "door", false},
|
||||||
|
{Floor, "room floor", false},
|
||||||
|
{PlayerCh, "you", false},
|
||||||
|
{Passage, "passage", false},
|
||||||
|
{Trap, "trap", false},
|
||||||
|
{Potion, "potion", false},
|
||||||
|
{Scroll, "scroll", false},
|
||||||
|
{Food, "food", false},
|
||||||
|
{Weapon, "weapon", false},
|
||||||
|
{' ', "solid rock", false},
|
||||||
|
{Armor, "armor", false},
|
||||||
|
{Amulet, "the Amulet of Yendor", false},
|
||||||
|
{Ring, "ring", false},
|
||||||
|
{Stick, "wand or staff", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
// identify tells the player what a certain thing is (command.c identify).
|
||||||
|
func (g *RogueGame) identify() {
|
||||||
|
g.msg("what do you want identified? ")
|
||||||
|
ch := g.readchar()
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
if ch == Escape {
|
||||||
|
g.msg("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var str string
|
||||||
|
if isUpper(ch) {
|
||||||
|
str = g.Monsters[ch-'A'].Name
|
||||||
|
} else {
|
||||||
|
str = "unknown character"
|
||||||
|
for _, hp := range identList {
|
||||||
|
if hp.Ch == ch {
|
||||||
|
str = hp.Desc
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.msg("'%s': %s", unctrl(ch), str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dLevel: he wants to go down a level (command.c d_level).
|
||||||
|
func (g *RogueGame) dLevel() {
|
||||||
|
if g.levitCheck() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) != Stairs {
|
||||||
|
g.msg("I see no way down")
|
||||||
|
} else {
|
||||||
|
g.Depth++
|
||||||
|
g.SeenStairs = false
|
||||||
|
g.NewLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// uLevel: he wants to go up a level (command.c u_level).
|
||||||
|
func (g *RogueGame) uLevel() {
|
||||||
|
if g.levitCheck() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X) == Stairs {
|
||||||
|
if g.HasAmulet {
|
||||||
|
g.Depth--
|
||||||
|
if g.Depth == 0 {
|
||||||
|
g.totalWinner()
|
||||||
|
}
|
||||||
|
g.NewLevel()
|
||||||
|
g.msg("you feel a wrenching sensation in your gut")
|
||||||
|
} else {
|
||||||
|
g.msg("your way is magically blocked")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g.msg("I see no way up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// levitCheck checks whether she's levitating, and if she is, prints an
|
||||||
|
// appropriate message (command.c levit_check).
|
||||||
|
func (g *RogueGame) levitCheck() bool {
|
||||||
|
if !g.Player.On(IsLevit) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
g.msg("You can't. You're floating off the ground!")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// call allows a user to call a potion, scroll, or ring something
|
||||||
|
// (command.c call).
|
||||||
|
func (g *RogueGame) call() {
|
||||||
|
obj := g.getItem("call", Callable)
|
||||||
|
// Make certain that it is something that we want to name
|
||||||
|
if obj == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var op *ObjInfo
|
||||||
|
var elsewise string
|
||||||
|
var know *bool
|
||||||
|
var guess *string
|
||||||
|
it := &g.Items
|
||||||
|
switch obj.Type {
|
||||||
|
case Ring:
|
||||||
|
op = &it.Rings[obj.Which]
|
||||||
|
elsewise = it.RingStones[obj.Which]
|
||||||
|
case Potion:
|
||||||
|
op = &it.Potions[obj.Which]
|
||||||
|
elsewise = it.PotColors[obj.Which]
|
||||||
|
case Scroll:
|
||||||
|
op = &it.Scrolls[obj.Which]
|
||||||
|
elsewise = it.ScrNames[obj.Which]
|
||||||
|
case Stick:
|
||||||
|
op = &it.Sticks[obj.Which]
|
||||||
|
elsewise = it.WandMade[obj.Which]
|
||||||
|
case Food:
|
||||||
|
g.msg("you can't call that anything")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
guess = &obj.Label
|
||||||
|
elsewise = obj.Label
|
||||||
|
}
|
||||||
|
fromGuess := false
|
||||||
|
if op != nil {
|
||||||
|
know = &op.Know
|
||||||
|
guess = &op.Guess
|
||||||
|
if *guess != "" {
|
||||||
|
elsewise = *guess
|
||||||
|
fromGuess = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fromGuess = elsewise != "" && elsewise == *guess
|
||||||
|
}
|
||||||
|
if know != nil && *know {
|
||||||
|
g.msg("that has already been identified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fromGuess {
|
||||||
|
if !g.Options.Terse {
|
||||||
|
g.addmsg("Was ")
|
||||||
|
}
|
||||||
|
g.msg("called \"%s\"", elsewise)
|
||||||
|
}
|
||||||
|
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
|
||||||
|
|
||||||
|
buf := elsewise
|
||||||
|
if g.getStr(&buf, g.scr.Std) == Norm {
|
||||||
|
*guess = buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// current prints the current weapon/armor (command.c current).
|
||||||
|
func (g *RogueGame) current(cur *Object, how, where string) {
|
||||||
|
g.After = false
|
||||||
|
if cur != nil {
|
||||||
|
if !g.Options.Terse {
|
||||||
|
g.addmsg("you are %s (", how)
|
||||||
|
}
|
||||||
|
g.InvDescribe = false
|
||||||
|
g.addmsg("%c) %s", cur.PackCh, g.invName(cur, true))
|
||||||
|
g.InvDescribe = true
|
||||||
|
if where != "" {
|
||||||
|
g.addmsg(" %s", where)
|
||||||
|
}
|
||||||
|
g.endmsg()
|
||||||
|
} else {
|
||||||
|
if !g.Options.Terse {
|
||||||
|
g.addmsg("you are ")
|
||||||
|
}
|
||||||
|
g.addmsg("%s nothing", how)
|
||||||
|
if where != "" {
|
||||||
|
g.addmsg(" %s", where)
|
||||||
|
}
|
||||||
|
g.endmsg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shell lets them escape for a while (main.c shell). The terminal decides
|
||||||
|
// how to actually suspend; headless terminals just decline.
|
||||||
|
func (g *RogueGame) shell() {
|
||||||
|
g.After = false
|
||||||
|
if se, ok := g.scr.term.(interface{ ShellEscape() }); ok {
|
||||||
|
g.InShell = true
|
||||||
|
se.ShellEscape()
|
||||||
|
g.InShell = false
|
||||||
|
g.refresh()
|
||||||
|
} else {
|
||||||
|
g.msg("shell escape is not available")
|
||||||
|
}
|
||||||
|
}
|
||||||
68
game/game.go
68
game/game.go
@@ -94,6 +94,10 @@ type RogueGame struct {
|
|||||||
LastPick *Object
|
LastPick *Object
|
||||||
LLastPick *Object
|
LLastPick *Object
|
||||||
lastDelt Coord // misc.c get_dir static last_delt
|
lastDelt Coord // misc.c get_dir static last_delt
|
||||||
|
// command.c statics
|
||||||
|
countCh byte
|
||||||
|
direction byte
|
||||||
|
newCount bool
|
||||||
|
|
||||||
// dungeon generation working state
|
// dungeon generation working state
|
||||||
maze mazeState // rooms.c maze statics
|
maze mazeState // rooms.c maze statics
|
||||||
@@ -131,6 +135,9 @@ type RogueGame struct {
|
|||||||
LastScore int
|
LastScore int
|
||||||
AllScore bool
|
AllScore bool
|
||||||
ScorePath string
|
ScorePath string
|
||||||
|
|
||||||
|
rogueOpts string // the ROGUEOPTS string, re-parsed by playit as in C
|
||||||
|
restored bool // game came from a save file; Run skips setup
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGame builds a game from cfg, seeds the RNG, and randomizes the item
|
// NewGame builds a game from cfg, seeds the RNG, and randomizes the item
|
||||||
@@ -159,10 +166,13 @@ func NewGame(cfg Config) *RogueGame {
|
|||||||
g.Msgs.SaveMsg = true
|
g.Msgs.SaveMsg = true
|
||||||
g.scr = NewScreen(cfg.Term)
|
g.scr = NewScreen(cfg.Term)
|
||||||
g.FileName = cfg.Home + "/rogue.save"
|
g.FileName = cfg.Home + "/rogue.save"
|
||||||
|
g.rogueOpts = cfg.RogueOpts
|
||||||
if cfg.Wizard {
|
if cfg.Wizard {
|
||||||
g.Player.Flags.Set(SeeMonst)
|
g.Player.Flags.Set(SeeMonst)
|
||||||
}
|
}
|
||||||
// TODO(port): parse cfg.RogueOpts once options.go lands.
|
if cfg.RogueOpts != "" {
|
||||||
|
g.ParseOpts(cfg.RogueOpts)
|
||||||
|
}
|
||||||
|
|
||||||
g.Monsters = monsterTable
|
g.Monsters = monsterTable
|
||||||
g.Items.Group = 2 // weapons.c: int group = 2
|
g.Items.Group = 2 // weapons.c: int group = 2
|
||||||
@@ -179,6 +189,62 @@ func NewGame(cfg Config) *RogueGame {
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run plays the game to its end: the back half of main.c main() plus
|
||||||
|
// playit(). It returns after death, victory, quitting, or saving.
|
||||||
|
func (g *RogueGame) Run() (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if _, ok := r.(gameEnd); ok {
|
||||||
|
return // normal game over / save exit
|
||||||
|
}
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if !g.restored {
|
||||||
|
g.NewLevel() // draw current level
|
||||||
|
// Start up daemons and fuses
|
||||||
|
g.StartDaemon(DRunners, 0, After)
|
||||||
|
g.StartDaemon(DDoctor, 0, After)
|
||||||
|
g.Fuse(DSwander, 0, wanderTime(g), After)
|
||||||
|
g.StartDaemon(DStomach, 0, After)
|
||||||
|
}
|
||||||
|
g.playit()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// playit is the main loop of the program (main.c playit).
|
||||||
|
func (g *RogueGame) playit() {
|
||||||
|
// set up defaults for modern terminals: curses' md_hasclreol() is
|
||||||
|
// always true, so the C default inventory style applies
|
||||||
|
if !g.restored {
|
||||||
|
g.Options.InvType = InvClear
|
||||||
|
}
|
||||||
|
// parse environment declaration of options (C parses ROGUEOPTS again
|
||||||
|
// here, letting it override the terminal defaults)
|
||||||
|
if g.rogueOpts != "" {
|
||||||
|
g.ParseOpts(g.rogueOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Oldpos = g.Player.Pos
|
||||||
|
g.Oldrp = g.roomin(g.Player.Pos)
|
||||||
|
for g.Playing {
|
||||||
|
g.command() // command execution
|
||||||
|
}
|
||||||
|
g.endit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// endit exits the game (main.c endit).
|
||||||
|
func (g *RogueGame) endit() {
|
||||||
|
g.fatal("Okay, bye bye!\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fatal prints a message and leaves (main.c fatal).
|
||||||
|
func (g *RogueGame) fatal(s string) {
|
||||||
|
g.mvaddstr(NumLines-2, 0, s)
|
||||||
|
g.refresh()
|
||||||
|
g.myExit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// quit has the player make certain, then exits (main.c quit). The final
|
// quit has the player make certain, then exits (main.c quit). The final
|
||||||
// scoring display arrives with the endgame phase.
|
// scoring display arrives with the endgame phase.
|
||||||
func (g *RogueGame) quit(int) {
|
func (g *RogueGame) quit(int) {
|
||||||
|
|||||||
@@ -50,23 +50,6 @@ func (l *Level) VisibleChar(y, x int) byte {
|
|||||||
return l.Char(y, x)
|
return l.Char(y, x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset clears the per-level state ahead of NewLevel drawing a fresh level.
|
|
||||||
func (l *Level) reset() {
|
|
||||||
for i := range l.Places {
|
|
||||||
l.Places[i] = Place{}
|
|
||||||
}
|
|
||||||
for i := range l.Rooms {
|
|
||||||
l.Rooms[i] = Room{}
|
|
||||||
}
|
|
||||||
for i := range l.Passages {
|
|
||||||
l.Passages[i] = Room{Flags: IsGone | IsDark}
|
|
||||||
}
|
|
||||||
l.Objects = nil
|
|
||||||
l.Monsters = nil
|
|
||||||
l.Stairs = Coord{}
|
|
||||||
l.NTraps = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// goldCalc is the GOLDCALC macro: how much a gold pile is worth at depth.
|
// goldCalc is the GOLDCALC macro: how much a gold pile is worth at depth.
|
||||||
func (g *RogueGame) goldCalc() int {
|
func (g *RogueGame) goldCalc() int {
|
||||||
return g.rnd(50+10*g.Depth) + 2
|
return g.rnd(50+10*g.Depth) + 2
|
||||||
|
|||||||
@@ -246,6 +246,38 @@ func (g *RogueGame) door(rm *Room, cp Coord) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addPass adds the passages to the current window — wizard command
|
||||||
|
// (passages.c add_pass).
|
||||||
|
func (g *RogueGame) addPass() {
|
||||||
|
for y := 1; y < NumLines-1; y++ {
|
||||||
|
for x := 0; x < NumCols; x++ {
|
||||||
|
pp := g.Level.At(y, x)
|
||||||
|
if pp.Flags.Has(FPass) || pp.Ch == Door ||
|
||||||
|
(!pp.Flags.Has(FReal) && (pp.Ch == '|' || pp.Ch == '-')) {
|
||||||
|
ch := pp.Ch
|
||||||
|
if pp.Flags.Has(FPass) {
|
||||||
|
ch = Passage
|
||||||
|
}
|
||||||
|
pp.Flags.Set(FSeen)
|
||||||
|
g.move(y, x)
|
||||||
|
if pp.Monst != nil {
|
||||||
|
pp.Monst.OldCh = pp.Ch
|
||||||
|
} else if pp.Flags.Has(FReal) {
|
||||||
|
g.addch(ch)
|
||||||
|
} else {
|
||||||
|
g.standout()
|
||||||
|
if pp.Flags.Has(FPass) {
|
||||||
|
g.addch(Passage)
|
||||||
|
} else {
|
||||||
|
g.addch(Door)
|
||||||
|
}
|
||||||
|
g.standend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// passnum assigns a number to each passageway (passages.c passnum).
|
// passnum assigns a number to each passageway (passages.c passnum).
|
||||||
func (g *RogueGame) passnum() {
|
func (g *RogueGame) passnum() {
|
||||||
g.pnum = 0
|
g.pnum = 0
|
||||||
|
|||||||
20
game/rip.go
20
game/rip.go
@@ -216,6 +216,26 @@ func (g *RogueGame) killname(monst byte, doart bool) string {
|
|||||||
return sp
|
return sp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeathDemo implements the -d command line option (main.c): burn some
|
||||||
|
// random numbers to break patterns, then die a random death.
|
||||||
|
func (g *RogueGame) DeathDemo() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if _, ok := r.(gameEnd); ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
dnum := g.rnd(100)
|
||||||
|
for dnum--; dnum > 0; dnum-- {
|
||||||
|
g.rnd(100)
|
||||||
|
}
|
||||||
|
g.Player.Purse = g.rnd(100) + 1
|
||||||
|
g.Depth = g.rnd(100) + 1
|
||||||
|
g.death(g.deathMonst())
|
||||||
|
}
|
||||||
|
|
||||||
// deathMonst returns a monster appropriate for a random death (rip.c
|
// deathMonst returns a monster appropriate for a random death (rip.c
|
||||||
// death_monst).
|
// death_monst).
|
||||||
func (g *RogueGame) deathMonst() byte {
|
func (g *RogueGame) deathMonst() byte {
|
||||||
|
|||||||
104
game/run_test.go
Normal file
104
game/run_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRunScriptedSession drives a complete game through Run(): a few
|
||||||
|
// moves, a rest, an inventory, then Q-quit answered yes.
|
||||||
|
func TestRunScriptedSession(t *testing.T) {
|
||||||
|
tt := &testTerm{input: []byte("hjkl.i Qy")}
|
||||||
|
g := NewGame(Config{Seed: 99, Term: tt})
|
||||||
|
if err := g.Run(); err != nil {
|
||||||
|
t.Fatalf("Run: %v", err)
|
||||||
|
}
|
||||||
|
if g.Playing {
|
||||||
|
t.Error("still playing after quit")
|
||||||
|
}
|
||||||
|
// After quitting, the scoreboard is the last thing shown (in C it went
|
||||||
|
// to stdout after endwin; here it is drawn on the screen).
|
||||||
|
found := false
|
||||||
|
for y := 0; y < NumLines; y++ {
|
||||||
|
if strings.Contains(g.scr.Std.Line(y), "Top Ten") {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("score list not on screen after quit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunManyTurns mashes movement keys for a while as a crash sweep of
|
||||||
|
// the whole turn loop (daemons, hunger, monsters, combat), ending with a
|
||||||
|
// quit. The input alternates directions so the hero bumps around rooms.
|
||||||
|
func TestRunManyTurns(t *testing.T) {
|
||||||
|
// Spaces between commands double as answers to any --More-- prompts;
|
||||||
|
// without them a single prompt would swallow the rest of the script
|
||||||
|
// (wait_for eats everything that isn't a space).
|
||||||
|
var script []byte
|
||||||
|
moves := []byte("h h j j k k l l y u b n s s . . ")
|
||||||
|
for range 200 {
|
||||||
|
script = append(script, moves...)
|
||||||
|
}
|
||||||
|
script = append(script, " Q y Qy"...)
|
||||||
|
tt := &testTerm{input: script}
|
||||||
|
g := NewGame(Config{Seed: 31337, Term: tt})
|
||||||
|
if err := g.Run(); err != nil {
|
||||||
|
t.Fatalf("Run: %v", err)
|
||||||
|
}
|
||||||
|
if g.Playing {
|
||||||
|
t.Error("session did not end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunDownStairs walks the hero onto the stairs by teleporting there in
|
||||||
|
// wizard style, then descends and keeps playing.
|
||||||
|
func TestRunDownStairs(t *testing.T) {
|
||||||
|
tt := &testTerm{input: []byte(">..Qy")}
|
||||||
|
g := NewGame(Config{Seed: 7, Term: tt})
|
||||||
|
g.NewLevel()
|
||||||
|
g.Player.Pos = g.Level.Stairs // stand on the stairs
|
||||||
|
g.restored = true // keep Run from regenerating the level
|
||||||
|
g.Daemons = DaemonList{} // and give it a fresh daemon table
|
||||||
|
g.StartDaemon(DRunners, 0, After)
|
||||||
|
g.StartDaemon(DDoctor, 0, After)
|
||||||
|
g.Fuse(DSwander, 0, wanderTime(g), After)
|
||||||
|
g.StartDaemon(DStomach, 0, After)
|
||||||
|
if err := g.Run(); err != nil {
|
||||||
|
t.Fatalf("Run: %v", err)
|
||||||
|
}
|
||||||
|
if g.Depth != 2 {
|
||||||
|
t.Errorf("depth = %d after descending, want 2", g.Depth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSaveCommandRoundTrip saves via the 'S' command (as a player would)
|
||||||
|
// and restores the game.
|
||||||
|
func TestSaveCommandRoundTrip(t *testing.T) {
|
||||||
|
// The C get_str caps input at MAXINP=50 characters, so the save path
|
||||||
|
// must be short: work from the temp directory.
|
||||||
|
t.Chdir(t.TempDir())
|
||||||
|
path := "cmd.save"
|
||||||
|
// 'S' with no default file name goes straight to the name prompt.
|
||||||
|
script := "S" + path + "\n"
|
||||||
|
tt := &testTerm{input: []byte(script)}
|
||||||
|
g := NewGame(Config{Seed: 55, Term: tt})
|
||||||
|
g.FileName = "" // force the name prompt
|
||||||
|
if err := g.Run(); err != nil {
|
||||||
|
t.Fatalf("Run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := Restore(path, Config{Term: &testTerm{}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Restore: %v", err)
|
||||||
|
}
|
||||||
|
if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse {
|
||||||
|
t.Error("restored game does not match saved game")
|
||||||
|
}
|
||||||
|
// The restored game must be playable.
|
||||||
|
h.scr.term.(*testTerm).input = []byte("..Qy")
|
||||||
|
if err := h.Run(); err != nil {
|
||||||
|
t.Fatalf("restored Run: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
553
game/save.go
Normal file
553
game/save.go
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// save.c + state.c — game persistence. The hand-written, XOR-encrypted C
|
||||||
|
// serializer becomes a gob snapshot; what remains hand-written is exactly
|
||||||
|
// what state.c's rs_fix_thing did: pointers that alias other structures
|
||||||
|
// (equipment, chase targets, room membership) are encoded as indices.
|
||||||
|
|
||||||
|
// destRef encodes a monster's chase target (state.c rs_write_thing's
|
||||||
|
// (kind, index) pairs).
|
||||||
|
type destRef struct {
|
||||||
|
Kind int // 0 none, 1 hero, 2 monster index, 3 level-object index, 4 room gold
|
||||||
|
Idx int
|
||||||
|
}
|
||||||
|
|
||||||
|
// savedCreature is the pointer-free image of a Creature.
|
||||||
|
type savedCreature struct {
|
||||||
|
Pos Coord
|
||||||
|
Turn bool
|
||||||
|
Type byte
|
||||||
|
Disguise byte
|
||||||
|
OldCh byte
|
||||||
|
Flags CreatureFlags
|
||||||
|
Stats Stats
|
||||||
|
RoomIdx int // 0..MaxRooms-1; 100+i for passages; -1 none
|
||||||
|
Pack []Object
|
||||||
|
}
|
||||||
|
|
||||||
|
// savedPlayer adds the player-only state. The creature half is a named
|
||||||
|
// field, not an embedded one: gob drops embedded fields of unexported
|
||||||
|
// types silently.
|
||||||
|
type savedPlayer struct {
|
||||||
|
Body savedCreature
|
||||||
|
CurArmor int // pack indices; -1 = none
|
||||||
|
CurWeapon int
|
||||||
|
CurRing [2]int
|
||||||
|
PackUsed [26]bool
|
||||||
|
Inpack int
|
||||||
|
Purse int
|
||||||
|
FoodLeft int
|
||||||
|
HungryState int
|
||||||
|
NoFood int
|
||||||
|
MaxStats Stats
|
||||||
|
VfHit int
|
||||||
|
}
|
||||||
|
|
||||||
|
// savedPlace is one map cell without its monster pointer.
|
||||||
|
type savedPlace struct {
|
||||||
|
Ch byte
|
||||||
|
Flags PlaceFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveState is the complete serialized game (the field list mirrors
|
||||||
|
// state.c rs_save_file).
|
||||||
|
type SaveState struct {
|
||||||
|
Version string
|
||||||
|
|
||||||
|
Seed int32
|
||||||
|
Dnum int
|
||||||
|
Whoami string
|
||||||
|
Fruit string
|
||||||
|
Home string
|
||||||
|
Wizard bool
|
||||||
|
|
||||||
|
Player savedPlayer
|
||||||
|
|
||||||
|
Depth int
|
||||||
|
MaxDepth int
|
||||||
|
HasAmulet bool
|
||||||
|
SeenStairs bool
|
||||||
|
|
||||||
|
Places []savedPlace
|
||||||
|
Rooms [MaxRooms]Room
|
||||||
|
Passages [MaxPass]Room
|
||||||
|
Objects []Object
|
||||||
|
Monsters []savedCreature
|
||||||
|
Dests []destRef // parallel to Monsters
|
||||||
|
Stairs Coord
|
||||||
|
NTraps int
|
||||||
|
|
||||||
|
After bool
|
||||||
|
Again bool
|
||||||
|
NoScoreF bool
|
||||||
|
Count int
|
||||||
|
NoCommand int
|
||||||
|
NoMove int
|
||||||
|
Quiet int
|
||||||
|
Running bool
|
||||||
|
RunCh byte
|
||||||
|
DoorStop bool
|
||||||
|
Firstmove bool
|
||||||
|
MoveOn bool
|
||||||
|
ToDeath bool
|
||||||
|
Kamikaze bool
|
||||||
|
MaxHit int
|
||||||
|
Take byte
|
||||||
|
Delta Coord
|
||||||
|
DirCh byte
|
||||||
|
LastComm byte
|
||||||
|
LastDir byte
|
||||||
|
LastPick int // pack index, -1
|
||||||
|
Oldpos Coord
|
||||||
|
OldrpIdx int
|
||||||
|
|
||||||
|
Daemons DaemonList
|
||||||
|
Options Options
|
||||||
|
Items ItemLore
|
||||||
|
Bestiary [26]MonsterKind
|
||||||
|
Huh string
|
||||||
|
|
||||||
|
LastScore int
|
||||||
|
AllScore bool
|
||||||
|
|
||||||
|
Screen []byte // the visible map (Std window contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// roomIdx encodes a room pointer as an index (state.c find_room_coord).
|
||||||
|
func (g *RogueGame) roomIdx(rp *Room) int {
|
||||||
|
if rp == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
for i := range g.Level.Rooms {
|
||||||
|
if rp == &g.Level.Rooms[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range g.Level.Passages {
|
||||||
|
if rp == &g.Level.Passages[i] {
|
||||||
|
return 100 + i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// roomAt decodes a room index.
|
||||||
|
func (g *RogueGame) roomAt(i int) *Room {
|
||||||
|
switch {
|
||||||
|
case i >= 100:
|
||||||
|
return &g.Level.Passages[i-100]
|
||||||
|
case i >= 0:
|
||||||
|
return &g.Level.Rooms[i]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// packIdx finds an object in the player's pack (state.c find_list_ptr).
|
||||||
|
func (g *RogueGame) packIdx(obj *Object) int {
|
||||||
|
if obj == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
for i, o := range g.Player.Pack {
|
||||||
|
if o == obj {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapshot captures the complete game state.
|
||||||
|
func (g *RogueGame) snapshot() *SaveState {
|
||||||
|
p := &g.Player
|
||||||
|
st := &SaveState{
|
||||||
|
Version: Release,
|
||||||
|
Seed: g.Rng.Seed,
|
||||||
|
Dnum: g.Dnum,
|
||||||
|
Whoami: g.Whoami,
|
||||||
|
Fruit: g.Fruit,
|
||||||
|
Home: g.Home,
|
||||||
|
Wizard: g.Wizard,
|
||||||
|
Depth: g.Depth,
|
||||||
|
MaxDepth: g.MaxDepth,
|
||||||
|
HasAmulet: g.HasAmulet,
|
||||||
|
SeenStairs: g.SeenStairs,
|
||||||
|
Rooms: g.Level.Rooms,
|
||||||
|
Passages: g.Level.Passages,
|
||||||
|
Stairs: g.Level.Stairs,
|
||||||
|
NTraps: g.Level.NTraps,
|
||||||
|
After: g.After,
|
||||||
|
Again: g.Again,
|
||||||
|
NoScoreF: g.NoScore,
|
||||||
|
Count: g.Count,
|
||||||
|
NoCommand: g.NoCommand,
|
||||||
|
NoMove: g.NoMove,
|
||||||
|
Quiet: g.Quiet,
|
||||||
|
Running: g.Running,
|
||||||
|
RunCh: g.RunCh,
|
||||||
|
DoorStop: g.DoorStop,
|
||||||
|
Firstmove: g.Firstmove,
|
||||||
|
MoveOn: g.MoveOn,
|
||||||
|
ToDeath: g.ToDeath,
|
||||||
|
Kamikaze: g.Kamikaze,
|
||||||
|
MaxHit: g.MaxHit,
|
||||||
|
Take: g.Take,
|
||||||
|
Delta: g.Delta,
|
||||||
|
DirCh: g.DirCh,
|
||||||
|
LastComm: g.LastComm,
|
||||||
|
LastDir: g.LastDir,
|
||||||
|
LastPick: g.packIdx(g.LastPick),
|
||||||
|
Oldpos: g.Oldpos,
|
||||||
|
OldrpIdx: g.roomIdx(g.Oldrp),
|
||||||
|
Daemons: g.Daemons,
|
||||||
|
Options: g.Options,
|
||||||
|
Items: g.Items,
|
||||||
|
Bestiary: g.Monsters,
|
||||||
|
Huh: g.Msgs.Huh,
|
||||||
|
LastScore: g.LastScore,
|
||||||
|
AllScore: g.AllScore,
|
||||||
|
Screen: g.scr.Std.Contents(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// the map, sans monster pointers (rebuilt on load)
|
||||||
|
st.Places = make([]savedPlace, len(g.Level.Places))
|
||||||
|
for i := range g.Level.Places {
|
||||||
|
st.Places[i] = savedPlace{
|
||||||
|
Ch: g.Level.Places[i].Ch,
|
||||||
|
Flags: g.Level.Places[i].Flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// level objects by value; remember their pointers for dest encoding
|
||||||
|
objAt := make(map[*Object]int, len(g.Level.Objects))
|
||||||
|
for i, o := range g.Level.Objects {
|
||||||
|
st.Objects = append(st.Objects, *o)
|
||||||
|
objAt[o] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// the player
|
||||||
|
st.Player = savedPlayer{
|
||||||
|
Body: savedCreature{
|
||||||
|
Pos: p.Pos, Turn: p.Turn, Type: p.Type, Disguise: p.Disguise,
|
||||||
|
OldCh: p.OldCh, Flags: p.Flags, Stats: p.Stats,
|
||||||
|
RoomIdx: g.roomIdx(p.Room),
|
||||||
|
},
|
||||||
|
CurArmor: g.packIdx(p.CurArmor),
|
||||||
|
CurWeapon: g.packIdx(p.CurWeapon),
|
||||||
|
CurRing: [2]int{
|
||||||
|
g.packIdx(p.CurRing[Left]), g.packIdx(p.CurRing[Right]),
|
||||||
|
},
|
||||||
|
PackUsed: p.PackUsed, Inpack: p.Inpack, Purse: p.Purse,
|
||||||
|
FoodLeft: p.FoodLeft, HungryState: p.HungryState, NoFood: p.NoFood,
|
||||||
|
MaxStats: p.MaxStats, VfHit: p.VfHit,
|
||||||
|
}
|
||||||
|
for _, o := range p.Pack {
|
||||||
|
st.Player.Body.Pack = append(st.Player.Body.Pack, *o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// monsters, with chase targets as (kind, index) references
|
||||||
|
for _, m := range g.Level.Monsters {
|
||||||
|
sc := savedCreature{
|
||||||
|
Pos: m.Pos, Turn: m.Turn, Type: m.Type, Disguise: m.Disguise,
|
||||||
|
OldCh: m.OldCh, Flags: m.Flags, Stats: m.Stats,
|
||||||
|
RoomIdx: g.roomIdx(m.Room),
|
||||||
|
}
|
||||||
|
for _, o := range m.Pack {
|
||||||
|
sc.Pack = append(sc.Pack, *o)
|
||||||
|
}
|
||||||
|
st.Monsters = append(st.Monsters, sc)
|
||||||
|
|
||||||
|
ref := destRef{}
|
||||||
|
switch {
|
||||||
|
case m.Dest == nil:
|
||||||
|
case m.Dest == &p.Pos:
|
||||||
|
ref = destRef{Kind: 1}
|
||||||
|
default:
|
||||||
|
for mi, om := range g.Level.Monsters {
|
||||||
|
if m.Dest == &om.Pos {
|
||||||
|
ref = destRef{Kind: 2, Idx: mi}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ref.Kind == 0 {
|
||||||
|
for _, oo := range g.Level.Objects {
|
||||||
|
if m.Dest == &oo.Pos {
|
||||||
|
ref = destRef{Kind: 3, Idx: objAt[oo]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ref.Kind == 0 {
|
||||||
|
for ri := range g.Level.Rooms {
|
||||||
|
if m.Dest == &g.Level.Rooms[ri].Gold {
|
||||||
|
ref = destRef{Kind: 4, Idx: ri}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.Dests = append(st.Dests, ref)
|
||||||
|
}
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
// applySnapshot rebuilds live game state from a snapshot.
|
||||||
|
func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||||
|
p := &g.Player
|
||||||
|
g.Rng.Seed = st.Seed
|
||||||
|
g.Dnum = st.Dnum
|
||||||
|
g.Whoami = st.Whoami
|
||||||
|
g.Fruit = st.Fruit
|
||||||
|
g.Home = st.Home
|
||||||
|
g.Wizard = st.Wizard
|
||||||
|
g.Depth = st.Depth
|
||||||
|
g.MaxDepth = st.MaxDepth
|
||||||
|
g.HasAmulet = st.HasAmulet
|
||||||
|
g.SeenStairs = st.SeenStairs
|
||||||
|
g.Level.Rooms = st.Rooms
|
||||||
|
g.Level.Passages = st.Passages
|
||||||
|
g.Level.Stairs = st.Stairs
|
||||||
|
g.Level.NTraps = st.NTraps
|
||||||
|
g.After = st.After
|
||||||
|
g.Again = st.Again
|
||||||
|
g.NoScore = st.NoScoreF
|
||||||
|
g.Count = st.Count
|
||||||
|
g.NoCommand = st.NoCommand
|
||||||
|
g.NoMove = st.NoMove
|
||||||
|
g.Quiet = st.Quiet
|
||||||
|
g.Running = st.Running
|
||||||
|
g.RunCh = st.RunCh
|
||||||
|
g.DoorStop = st.DoorStop
|
||||||
|
g.Firstmove = st.Firstmove
|
||||||
|
g.MoveOn = st.MoveOn
|
||||||
|
g.ToDeath = st.ToDeath
|
||||||
|
g.Kamikaze = st.Kamikaze
|
||||||
|
g.MaxHit = st.MaxHit
|
||||||
|
g.Take = st.Take
|
||||||
|
g.Delta = st.Delta
|
||||||
|
g.DirCh = st.DirCh
|
||||||
|
g.LastComm = st.LastComm
|
||||||
|
g.LastDir = st.LastDir
|
||||||
|
g.Oldpos = st.Oldpos
|
||||||
|
g.Oldrp = g.roomAt(st.OldrpIdx)
|
||||||
|
g.Daemons = st.Daemons
|
||||||
|
g.Options = st.Options
|
||||||
|
g.Items = st.Items
|
||||||
|
g.Monsters = st.Bestiary
|
||||||
|
g.Msgs.Huh = st.Huh
|
||||||
|
g.LastScore = st.LastScore
|
||||||
|
g.AllScore = st.AllScore
|
||||||
|
g.Playing = true
|
||||||
|
|
||||||
|
for i := range g.Level.Places {
|
||||||
|
g.Level.Places[i] = Place{
|
||||||
|
Ch: st.Places[i].Ch,
|
||||||
|
Flags: st.Places[i].Flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// level objects
|
||||||
|
g.Level.Objects = nil
|
||||||
|
for i := range st.Objects {
|
||||||
|
o := st.Objects[i]
|
||||||
|
g.Level.Objects = append(g.Level.Objects, &o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the player
|
||||||
|
sp := &st.Player
|
||||||
|
p.Pos = sp.Body.Pos
|
||||||
|
p.Turn = sp.Body.Turn
|
||||||
|
p.Type = sp.Body.Type
|
||||||
|
p.Disguise = sp.Body.Disguise
|
||||||
|
p.OldCh = sp.Body.OldCh
|
||||||
|
p.Flags = sp.Body.Flags
|
||||||
|
p.Stats = sp.Body.Stats
|
||||||
|
p.Room = g.roomAt(sp.Body.RoomIdx)
|
||||||
|
p.Pack = nil
|
||||||
|
for i := range sp.Body.Pack {
|
||||||
|
o := sp.Body.Pack[i]
|
||||||
|
p.Pack = append(p.Pack, &o)
|
||||||
|
}
|
||||||
|
pick := func(i int) *Object {
|
||||||
|
if i < 0 || i >= len(p.Pack) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.Pack[i]
|
||||||
|
}
|
||||||
|
p.CurArmor = pick(sp.CurArmor)
|
||||||
|
p.CurWeapon = pick(sp.CurWeapon)
|
||||||
|
p.CurRing[Left] = pick(sp.CurRing[0])
|
||||||
|
p.CurRing[Right] = pick(sp.CurRing[1])
|
||||||
|
g.LastPick = pick(st.LastPick)
|
||||||
|
p.PackUsed = sp.PackUsed
|
||||||
|
p.Inpack = sp.Inpack
|
||||||
|
p.Purse = sp.Purse
|
||||||
|
p.FoodLeft = sp.FoodLeft
|
||||||
|
p.HungryState = sp.HungryState
|
||||||
|
p.NoFood = sp.NoFood
|
||||||
|
p.MaxStats = sp.MaxStats
|
||||||
|
p.VfHit = sp.VfHit
|
||||||
|
|
||||||
|
// monsters, their map index, and their chase targets
|
||||||
|
g.Level.Monsters = nil
|
||||||
|
for i := range st.Monsters {
|
||||||
|
sc := &st.Monsters[i]
|
||||||
|
m := &Monster{Creature: Creature{
|
||||||
|
Pos: sc.Pos, Turn: sc.Turn, Type: sc.Type, Disguise: sc.Disguise,
|
||||||
|
OldCh: sc.OldCh, Flags: sc.Flags, Stats: sc.Stats,
|
||||||
|
Room: g.roomAt(sc.RoomIdx),
|
||||||
|
}}
|
||||||
|
for j := range sc.Pack {
|
||||||
|
o := sc.Pack[j]
|
||||||
|
m.Pack = append(m.Pack, &o)
|
||||||
|
}
|
||||||
|
g.Level.Monsters = append(g.Level.Monsters, m)
|
||||||
|
g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m)
|
||||||
|
}
|
||||||
|
for i, ref := range st.Dests {
|
||||||
|
m := g.Level.Monsters[i]
|
||||||
|
switch ref.Kind {
|
||||||
|
case 1:
|
||||||
|
m.Dest = &p.Pos
|
||||||
|
case 2:
|
||||||
|
m.Dest = &g.Level.Monsters[ref.Idx].Pos
|
||||||
|
case 3:
|
||||||
|
m.Dest = &g.Level.Objects[ref.Idx].Pos
|
||||||
|
case 4:
|
||||||
|
m.Dest = &g.Level.Rooms[ref.Idx].Gold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.scr.Std.SetContents(st.Screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveGame implements the "save game" command (save.c save_game). The C
|
||||||
|
// goto over/gotfile flow becomes the useDefault flag.
|
||||||
|
func (g *RogueGame) saveGame() {
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
over:
|
||||||
|
useDefault := false
|
||||||
|
if g.FileName != "" {
|
||||||
|
var c byte
|
||||||
|
for {
|
||||||
|
g.msg("save file (%s)? ", g.FileName)
|
||||||
|
c = g.readchar()
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
if c == Escape {
|
||||||
|
g.msg("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c == 'n' || c == 'N' || c == 'y' || c == 'Y' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
g.msg("please answer Y or N")
|
||||||
|
}
|
||||||
|
if c == 'y' || c == 'Y' {
|
||||||
|
g.addstr("Yes\n")
|
||||||
|
g.refresh()
|
||||||
|
useDefault = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
var buf string
|
||||||
|
if useDefault {
|
||||||
|
buf = g.FileName
|
||||||
|
useDefault = false
|
||||||
|
} else {
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
g.msg("file name: ")
|
||||||
|
if g.getStr(&buf, g.scr.Std) == Quit {
|
||||||
|
g.msg("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
}
|
||||||
|
// test to see if the file exists
|
||||||
|
if _, err := os.Stat(buf); err == nil {
|
||||||
|
for {
|
||||||
|
g.msg("File exists. Do you wish to overwrite it?")
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
c := g.readchar()
|
||||||
|
if c == Escape {
|
||||||
|
g.msg("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c == 'y' || c == 'Y' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if c == 'n' || c == 'N' {
|
||||||
|
goto over
|
||||||
|
}
|
||||||
|
g.msg("Please answer Y or N")
|
||||||
|
}
|
||||||
|
g.msg("file name: %s", buf)
|
||||||
|
os.Remove(g.FileName)
|
||||||
|
}
|
||||||
|
g.FileName = buf
|
||||||
|
if err := g.saveFile(g.FileName); err != nil {
|
||||||
|
g.msg("%s", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
g.myExit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveFile writes the saved game (save.c save_file).
|
||||||
|
func (g *RogueGame) saveFile(path string) error {
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o400)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if err := gob.NewEncoder(f).Encode(g.snapshot()); err != nil {
|
||||||
|
os.Remove(path)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Chmod(path, 0o400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoSave silently saves to the current file name; used on SIGHUP/SIGTERM
|
||||||
|
// (save.c auto_save).
|
||||||
|
func (g *RogueGame) AutoSave() {
|
||||||
|
if g.FileName != "" {
|
||||||
|
os.Remove(g.FileName)
|
||||||
|
g.saveFile(g.FileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores a saved game from a file (save.c restore). The file is
|
||||||
|
// deleted, as in C, to defeat restarting from the same save.
|
||||||
|
func Restore(path string, cfg Config) (*RogueGame, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
var st SaveState
|
||||||
|
if err := gob.NewDecoder(f).Decode(&st); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: corrupt or incompatible save file: %w", path, err)
|
||||||
|
}
|
||||||
|
if st.Version != Release {
|
||||||
|
return nil, fmt.Errorf("sorry, saved game is out of date")
|
||||||
|
}
|
||||||
|
|
||||||
|
g := &RogueGame{
|
||||||
|
Rng: &Rng{},
|
||||||
|
Playing: true,
|
||||||
|
ScorePath: cfg.ScorePath,
|
||||||
|
FileName: path,
|
||||||
|
rogueOpts: cfg.RogueOpts,
|
||||||
|
restored: true,
|
||||||
|
}
|
||||||
|
g.scr = NewScreen(cfg.Term)
|
||||||
|
g.applySnapshot(&st)
|
||||||
|
|
||||||
|
// defeat multiple restarting from the same place
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot unlink file: %w", err)
|
||||||
|
}
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
112
game/save_test.go
Normal file
112
game/save_test.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSaveRestoreRoundTrip(t *testing.T) {
|
||||||
|
g := mkGame(t, 4242)
|
||||||
|
// Dirty up some state so the round trip is meaningful.
|
||||||
|
g.Player.Purse = 123
|
||||||
|
g.Player.FoodLeft = 777
|
||||||
|
g.HasAmulet = true
|
||||||
|
g.Items.Potions[PHealing].Know = true
|
||||||
|
g.Items.Scrolls[SMap].Guess = "map???"
|
||||||
|
g.Monsters['F'-'A'].Stats.Dmg = "3x1" // mutated bestiary must survive
|
||||||
|
if len(g.Level.Monsters) > 0 {
|
||||||
|
g.Level.Monsters[0].Flags.Set(IsRun)
|
||||||
|
g.Level.Monsters[0].Dest = &g.Player.Pos
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "rogue.save")
|
||||||
|
if err := g.saveFile(path); err != nil {
|
||||||
|
t.Fatalf("saveFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := Restore(path, Config{Term: &testTerm{}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Restore: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||||
|
t.Error("save file not deleted on restore (C anti-restart rule)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Player.Purse != 123 || h.Player.FoodLeft != 777 {
|
||||||
|
t.Errorf("player state lost: purse=%d food=%d",
|
||||||
|
h.Player.Purse, h.Player.FoodLeft)
|
||||||
|
}
|
||||||
|
if !h.HasAmulet {
|
||||||
|
t.Error("amulet flag lost")
|
||||||
|
}
|
||||||
|
if !h.Items.Potions[PHealing].Know {
|
||||||
|
t.Error("potion identification lost")
|
||||||
|
}
|
||||||
|
if h.Items.Scrolls[SMap].Guess != "map???" {
|
||||||
|
t.Error("scroll guess lost")
|
||||||
|
}
|
||||||
|
if h.Monsters['F'-'A'].Stats.Dmg != "3x1" {
|
||||||
|
t.Error("mutated bestiary lost")
|
||||||
|
}
|
||||||
|
if h.Rng.Seed != g.Rng.Seed {
|
||||||
|
t.Error("RNG state lost")
|
||||||
|
}
|
||||||
|
if renderMap(h) != renderMap(g) {
|
||||||
|
t.Error("restored level map differs")
|
||||||
|
}
|
||||||
|
if len(h.Level.Monsters) != len(g.Level.Monsters) {
|
||||||
|
t.Fatalf("monster count %d != %d",
|
||||||
|
len(h.Level.Monsters), len(g.Level.Monsters))
|
||||||
|
}
|
||||||
|
if len(g.Level.Monsters) > 0 {
|
||||||
|
m := h.Level.Monsters[0]
|
||||||
|
if m.Dest != &h.Player.Pos {
|
||||||
|
t.Error("monster chase target not re-aliased to the hero")
|
||||||
|
}
|
||||||
|
if h.Level.MonsterAt(m.Pos.Y, m.Pos.X) != m {
|
||||||
|
t.Error("map monster index not rebuilt")
|
||||||
|
}
|
||||||
|
if m.Room == nil {
|
||||||
|
t.Error("monster room pointer not rebuilt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Equipment aliasing: the wielded mace must be the same *Object as the
|
||||||
|
// one in the pack.
|
||||||
|
st := g.snapshot()
|
||||||
|
t.Logf("snapshot indices: weapon=%d armor=%d rings=%v packlen=%d",
|
||||||
|
st.Player.CurWeapon, st.Player.CurArmor, st.Player.CurRing,
|
||||||
|
len(st.Player.Body.Pack))
|
||||||
|
t.Logf("restored: CurWeapon=%p pack has %d items", h.Player.CurWeapon,
|
||||||
|
len(h.Player.Pack))
|
||||||
|
found := false
|
||||||
|
for i, o := range h.Player.Pack {
|
||||||
|
t.Logf(" pack[%d]=%p type=%c which=%d", i, o, o.Type, o.Which)
|
||||||
|
if o == h.Player.CurWeapon {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("restored CurWeapon is not aliased into the pack")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRestoreRejectsWrongVersion(t *testing.T) {
|
||||||
|
g := mkGame(t, 1)
|
||||||
|
path := filepath.Join(t.TempDir(), "rogue.save")
|
||||||
|
st := g.snapshot()
|
||||||
|
st.Version = "0.0.0"
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := gob.NewEncoder(f).Encode(st); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
if _, err := Restore(path, Config{}); err == nil {
|
||||||
|
t.Error("restore accepted an out-of-date save")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,35 @@ func (w *Window) CopyFrom(src *Window) {
|
|||||||
copy(w.cells, src.cells)
|
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/
|
// Line returns row y as a trimmed string; used by tests and the death/
|
||||||
// victory screens.
|
// victory screens.
|
||||||
func (w *Window) Line(y int) string {
|
func (w *Window) Line(y int) string {
|
||||||
|
|||||||
@@ -1,8 +1,96 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
// wizard.c — special wizard commands, some of which are also non-wizard
|
// wizard.c — special wizard commands, some of which are also non-wizard
|
||||||
// commands under strange circumstances. The MASTER-only debug commands
|
// commands under strange circumstances. The DES password check is not
|
||||||
// (create_obj, show_map) arrive with the UI phase.
|
// ported: wizard mode is enabled by configuration instead.
|
||||||
|
|
||||||
|
// createObj is the wizard command for getting anything he wants (wizard.c
|
||||||
|
// create_obj).
|
||||||
|
func (g *RogueGame) createObj() {
|
||||||
|
obj := newObject()
|
||||||
|
g.msg("type of item: ")
|
||||||
|
obj.Type = g.readchar()
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
g.msg("which %c do you want? (0-f)", obj.Type)
|
||||||
|
ch := g.readchar()
|
||||||
|
if isDigit(ch) {
|
||||||
|
obj.Which = int(ch - '0')
|
||||||
|
} else {
|
||||||
|
obj.Which = int(ch-'a') + 10
|
||||||
|
}
|
||||||
|
obj.Group = 0
|
||||||
|
obj.Count = 1
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
switch {
|
||||||
|
case obj.Type == Weapon || obj.Type == Armor:
|
||||||
|
g.msg("blessing? (+,-,n)")
|
||||||
|
bless := g.readchar()
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
if bless == '-' {
|
||||||
|
obj.Flags.Set(IsCursed)
|
||||||
|
}
|
||||||
|
if obj.Type == Weapon {
|
||||||
|
g.initWeapon(obj, obj.Which)
|
||||||
|
if bless == '-' {
|
||||||
|
obj.HPlus -= g.rnd(3) + 1
|
||||||
|
}
|
||||||
|
if bless == '+' {
|
||||||
|
obj.HPlus += g.rnd(3) + 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj.Arm = aClass[obj.Which]
|
||||||
|
if bless == '-' {
|
||||||
|
obj.Arm += g.rnd(3) + 1
|
||||||
|
}
|
||||||
|
if bless == '+' {
|
||||||
|
obj.Arm -= g.rnd(3) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case obj.Type == Ring:
|
||||||
|
switch obj.Which {
|
||||||
|
case RProtect, RAddStr, RAddHit, RAddDam:
|
||||||
|
g.msg("blessing? (+,-,n)")
|
||||||
|
bless := g.readchar()
|
||||||
|
g.Msgs.Mpos = 0
|
||||||
|
if bless == '-' {
|
||||||
|
obj.Flags.Set(IsCursed)
|
||||||
|
obj.Arm = -1
|
||||||
|
} else {
|
||||||
|
obj.Arm = g.rnd(2) + 1
|
||||||
|
}
|
||||||
|
case RAggr, RTeleport:
|
||||||
|
obj.Flags.Set(IsCursed)
|
||||||
|
}
|
||||||
|
case obj.Type == Stick:
|
||||||
|
g.fixStick(obj)
|
||||||
|
case obj.Type == Gold:
|
||||||
|
g.msg("how much?")
|
||||||
|
buf := ""
|
||||||
|
if g.getStr(&buf, g.scr.Std) == Norm {
|
||||||
|
obj.SetGoldVal(cAtoi(buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.addPack(obj, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// showMap prints out the whole map for the wizard (wizard.c show_map).
|
||||||
|
func (g *RogueGame) showMap() {
|
||||||
|
hw := g.scr.Hw
|
||||||
|
hw.Clear()
|
||||||
|
for y := 1; y < NumLines-1; y++ {
|
||||||
|
for x := 0; x < NumCols; x++ {
|
||||||
|
real := g.Level.FlagsAt(y, x).Has(FReal)
|
||||||
|
if !real {
|
||||||
|
hw.Standout(true)
|
||||||
|
}
|
||||||
|
hw.MvAddCh(y, x, g.Level.Char(y, x))
|
||||||
|
if !real {
|
||||||
|
hw.Standout(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.showWin("---More (level map)---")
|
||||||
|
}
|
||||||
|
|
||||||
// whatis identifies what a certain object is (wizard.c whatis).
|
// whatis identifies what a certain object is (wizard.c whatis).
|
||||||
func (g *RogueGame) whatis(insist bool, typ int) {
|
func (g *RogueGame) whatis(insist bool, typ int) {
|
||||||
|
|||||||
10
go.mod
10
go.mod
@@ -1,3 +1,13 @@
|
|||||||
module sneak.berlin/go/rogue
|
module sneak.berlin/go/rogue
|
||||||
|
|
||||||
go 1.25.7
|
go 1.25.7
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gdamore/encoding v1.0.1 // indirect
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.10 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/term v0.37.0 // indirect
|
||||||
|
golang.org/x/text v0.31.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
45
go.sum
Normal file
45
go.sum
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||||
|
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA=
|
||||||
|
github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
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