Merge refactor/constructor-style (refactor step 8: New/Params + os.Exit)
This commit is contained in:
@@ -1076,7 +1076,7 @@ that `game` itself has no third-party imports:
|
|||||||
go.mod module git.eeqj.de/sneak/rgoue
|
go.mod module git.eeqj.de/sneak/rgoue
|
||||||
cmd/rogue/main.go package main — flag parsing, constructs RogueGame, calls Run
|
cmd/rogue/main.go package main — flag parsing, constructs RogueGame, calls Run
|
||||||
game/ package game — the port, one .go file per .c file
|
game/ package game — the port, one .go file per .c file
|
||||||
game.go RogueGame struct, NewGame, Run (main.c)
|
game.go RogueGame struct, New, Run (main.c)
|
||||||
rng.go seeded LCG (main.c: rnd/roll + RN macro)
|
rng.go seeded LCG (main.c: rnd/roll + RN macro)
|
||||||
types.go Coord, Stats, flags, constants (rogue.h)
|
types.go Coord, Stats, flags, constants (rogue.h)
|
||||||
object.go Object type (rogue.h THING _o arm)
|
object.go Object type (rogue.h THING _o arm)
|
||||||
@@ -1120,7 +1120,7 @@ coherent, but everything is reachable from `g`.
|
|||||||
|
|
||||||
```go
|
```go
|
||||||
// RogueGame is one complete game of Rogue: all state that was global in the
|
// RogueGame is one complete game of Rogue: all state that was global in the
|
||||||
// C implementation, plus the terminal it plays on. Construct with NewGame,
|
// C implementation, plus the terminal it plays on. Construct with New,
|
||||||
// then call Run.
|
// then call Run.
|
||||||
type RogueGame struct {
|
type RogueGame struct {
|
||||||
// --- identity / RNG ---
|
// --- identity / RNG ---
|
||||||
@@ -1193,15 +1193,15 @@ The command.c/io.c/things.c file-statics become lowercase fields of `RogueGame`,
|
|||||||
### Entrypoints
|
### Entrypoints
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func NewGame(opts Config) *RogueGame // Config: seed, name, ROGUEOPTS string, score path, wizard
|
func New(params Params) *RogueGame // Params: seed, name, ROGUEOPTS string, score path, wizard
|
||||||
func (g *RogueGame) Run() error // main.c main()+playit(): init tables, first level, daemons, loop
|
func (g *RogueGame) Run() // main.c main()+playit(): init tables, first level, daemons, loop
|
||||||
func (g *RogueGame) command() // one turn (command.c)
|
func (g *RogueGame) command() // one turn (command.c)
|
||||||
func Restore(path string, opts Config) (*RogueGame, error) // save.c restore()
|
func Restore(path string, params Params) (*RogueGame, error) // save.c restore()
|
||||||
```
|
```
|
||||||
|
|
||||||
`cmd/rogue/main.go` is a thin shell: flags (`-s` scores, `-d` death demo,
|
`cmd/rogue/main.go` is a thin shell: flags (`-s` scores, `-d` death demo,
|
||||||
save-file arg, `SEED`/`ROGUEOPTS` env), `NewGame(...)` or `Restore(...)`,
|
save-file arg, `SEED`/`ROGUEOPTS` env), `New(...)` or `Restore(...)`, `Run()`,
|
||||||
`Run()`, exit code.
|
exit code.
|
||||||
|
|
||||||
## 4. Core types
|
## 4. Core types
|
||||||
|
|
||||||
@@ -1366,7 +1366,7 @@ type Room struct {
|
|||||||
tables that are never written after init** (the only permitted package-level
|
tables that are never written after init** (the only permitted package-level
|
||||||
data — they are ROM, not state): `monsterTable [26]MonsterKind` (name, carry %,
|
data — they are ROM, not state): `monsterTable [26]MonsterKind` (name, carry %,
|
||||||
flags, stats), `initDam` weapon table, base `objInfo` tables (copied into
|
flags, stats), `initDam` weapon table, base `objInfo` tables (copied into
|
||||||
`RogueGame.Items` at NewGame so per-game mutation — probability resumming,
|
`RogueGame.Items` at New so per-game mutation — probability resumming,
|
||||||
`oi_know`, `oi_guess` — stays instance-local), `eLevels`, `rainbow`, `stones`,
|
`oi_know`, `oi_guess` — stays instance-local), `eLevels`, `rainbow`, `stones`,
|
||||||
`woods`, `metals`, `sylls`, trap names, help text, `lvlMons`/`wandMons` spawn
|
`woods`, `metals`, `sylls`, trap names, help text, `lvlMons`/`wandMons` spawn
|
||||||
strings, `hNames`/`mNames` combat messages.
|
strings, `hNames`/`mNames` combat messages.
|
||||||
@@ -1552,26 +1552,26 @@ Tombstone/victory screens port verbatim from rip.c.
|
|||||||
|
|
||||||
## 6. C construct → Go construct map
|
## 6. C construct → Go construct map
|
||||||
|
|
||||||
| C construct | Go translation |
|
| C construct | Go translation |
|
||||||
| ---------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
| ---------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||||
| global variable | `RogueGame` field (or Player/Level/subsystem field) |
|
| global variable | `RogueGame` field (or Player/Level/subsystem field) |
|
||||||
| file-scope static | unexported field on `RogueGame` or subsystem struct |
|
| file-scope static | unexported field on `RogueGame` or subsystem struct |
|
||||||
| `THING` union | `Creature` / `Object` structs |
|
| `THING` union | `Creature` / `Object` structs |
|
||||||
| linked lists (`l_next/l_prev`, attach/detach) | slices + `attach(&s, x)`-style helpers (prepend, remove by identity) |
|
| linked lists (`l_next/l_prev`, attach/detach) | slices + `attach(&s, x)`-style helpers (prepend, remove by identity) |
|
||||||
| `coord*` aliasing (t_dest) | `*Coord` pointers into live structs (kept) |
|
| `coord*` aliasing (t_dest) | `*Coord` pointers into live structs (kept) |
|
||||||
| function pointers (daemons, options, nameit) | enum IDs + dispatch switch (daemons, save-relevant); closures (options, nameit) |
|
| function pointers (daemons, options, nameit) | enum IDs + dispatch switch (daemons, save-relevant); closures (options, nameit) |
|
||||||
| `when`/`otherwise`/`until` macros | `case`/`default`/`for !cond` |
|
| `when`/`otherwise`/`until` macros | `case`/`default`/`for !cond` |
|
||||||
| `goto` (retry loops in pack.c, move.c, passages.c) | labeled loops / restructured `for` |
|
| `goto` (retry loops in pack.c, move.c, passages.c) | labeled loops / restructured `for` |
|
||||||
| char-indexed tables (`monsters[type-'A']`) | same, `monsterTable[t.Type-'A']` |
|
| char-indexed tables (`monsters[type-'A']`) | same, `monsterTable[t.Type-'A']` |
|
||||||
| damage strings `"3x4/1x2"` | kept as strings; parsed by `rollEm` (`strings`/`strconv`) |
|
| damage strings `"3x4/1x2"` | kept as strings; parsed by `rollEm` (`strings`/`strconv`) |
|
||||||
| curses stdscr/hw windows | `Window` cell buffers over tcell |
|
| curses stdscr/hw windows | `Window` cell buffers over tcell |
|
||||||
| `mvinch` screen reads | `Window.Inch` from the buffer |
|
| `mvinch` screen reads | `Window.Inch` from the buffer |
|
||||||
| signal handlers (SIGHUP autosave, SIGTSTP, SIGINT) | `os/signal` goroutine → channel checked in ReadChar; tcell handles TSTP/resize |
|
| signal handlers (SIGHUP autosave, SIGTSTP, SIGINT) | `os/signal` goroutine → channel checked in ReadChar; tcell handles TSTP/resize |
|
||||||
| `setjmp`-free exits (`my_exit`, `exit()` everywhere) | error returns from `Run` + a small `gameOver` sentinel; no `os.Exit` inside game logic |
|
| `setjmp`-free exits (`my_exit`, `exit()` everywhere) | `myExit` restores the terminal and calls `os.Exit(0)`; one game run is one process |
|
||||||
| `vsprintf` message building | `fmt.Sprintf` |
|
| `vsprintf` message building | `fmt.Sprintf` |
|
||||||
| XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) |
|
| XOR-encrypted binary saves (state.c) | gob snapshot (§5.6) |
|
||||||
| DES wizard password | `ROGUE_WIZARD=1` env check |
|
| DES wizard password | `ROGUE_WIZARD=1` env check |
|
||||||
| `md_*` platform shims | stdlib (`os`, `os/user`, `time`) or deleted |
|
| `md_*` platform shims | stdlib (`os`, `os/user`, `time`) or deleted |
|
||||||
|
|
||||||
**Exit discipline** deserves a note: the C code calls `exit()` from deep inside
|
**Exit discipline** deserves a note: the C code calls `exit()` from deep inside
|
||||||
call chains (death, victory, save). The port threads a `g.gameOver(reason)` that
|
call chains (death, victory, save). The port threads a `g.gameOver(reason)` that
|
||||||
@@ -1634,6 +1634,6 @@ defers.
|
|||||||
- **Determinism test:** scripted input sequence against a fixed seed must
|
- **Determinism test:** scripted input sequence against a fixed seed must
|
||||||
produce identical final state across runs (guards hidden nondeterminism — map
|
produce identical final state across runs (guards hidden nondeterminism — map
|
||||||
iteration, time dependence).
|
iteration, time dependence).
|
||||||
- **Save round-trip:** NewGame → play N scripted turns → save → restore →
|
- **Save round-trip:** New → play N scripted turns → save → restore → compare
|
||||||
compare full state.
|
full state.
|
||||||
- `go vet` + `gofmt` clean at every commit.
|
- `go vet` + `gofmt` clean at every commit.
|
||||||
|
|||||||
41
TODO.md
41
TODO.md
@@ -29,12 +29,23 @@ Refactor ground rules:
|
|||||||
|
|
||||||
# Next Step
|
# Next Step
|
||||||
|
|
||||||
Refactor step 8: constructor and style pass per the house styleguide —
|
Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the post-refactor
|
||||||
game.New(game.Params{...}) replacing NewGame(Config); replace the gameEnd panic
|
names; add the C name → Go name rename table.
|
||||||
unwind with error-based turn results where feasible; 77-column wrap sweep.
|
|
||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-07-23 Refactor step 8 (refactor/constructor-style): constructor and exit
|
||||||
|
pass. NewGame(Config) → New(Params) and Restore takes Params, so the package's
|
||||||
|
primary type gets the canonical New() constructor with a named-field Params
|
||||||
|
struct (styleguide 139/159). The gameEnd panic unwind is gone: one game run is
|
||||||
|
one process, so myExit restores the terminal (new Terminal.Fini) and calls
|
||||||
|
os.Exit(0), and Run() no longer returns; the four Run()-to-completion tests
|
||||||
|
were reworked/dropped since death (combat or starvation) now exits the process
|
||||||
|
(TestScoreRendersList and TestRunDownStairs preserve what is still drivable;
|
||||||
|
save/restore stays covered by TestSaveRestoreRoundTrip). The 77-column wrap
|
||||||
|
sweep was dropped per sneak (2026-07-23): line lengths left as-is (lll caps at
|
||||||
|
88 and passes).
|
||||||
|
|
||||||
- 2026-07-07 Refactor step 7 (refactor/effects-dispatch): effects dispatch
|
- 2026-07-07 Refactor step 7 (refactor/effects-dispatch): effects dispatch
|
||||||
tables plus a full decomposition sweep — the quaff / readScroll / doZap
|
tables plus a full decomposition sweep — the quaff / readScroll / doZap
|
||||||
switches, the attack monster-power switch, the be_trapped switch, the daemon
|
switches, the attack monster-power switch, the be_trapped switch, the daemon
|
||||||
@@ -133,24 +144,24 @@ unwind with error-based turn results where feasible; 77-column wrap sweep.
|
|||||||
|
|
||||||
# Future Steps
|
# Future Steps
|
||||||
|
|
||||||
1. Docs refresh: update ARCHITECTURE.md Part 2 and README.md for the
|
1. Playtest hardening pass: play several full games with the tcell binary and
|
||||||
post-refactor names; add the C name → Go name rename table.
|
extend run_test.go to drive a deeper multi-level playthrough (descend past
|
||||||
2. Playtest hardening pass: play several full games with the tcell binary and
|
level 5, use potions, scrolls, zapping, save/restore). Note: game-over now
|
||||||
extend run_test.go to script a deeper multi-level playthrough (descend past
|
exits the process (step 8), so scripted drives must stay bounded and avoid
|
||||||
level 5, use potions, scrolls, zapping, save/restore). Fix any panics,
|
death/quit/save-command; exercise the exit paths (death display, 'S' save)
|
||||||
message mismatches, or divergences from the C behavior that this uncovers,
|
another way if needed. Fix any panics, message mismatches, or divergences
|
||||||
with regression tests.
|
from the C behavior that this uncovers, with regression tests.
|
||||||
3. Verify the seed-compatibility claim against the C reference on c-master: same
|
2. Verify the seed-compatibility claim against the C reference on c-master: same
|
||||||
seed, same dungeon, same item tables, for several seeds.
|
seed, same dungeon, same item tables, for several seeds.
|
||||||
4. Broaden unit test coverage where playtesting finds thin spots (rings, sticks,
|
3. Broaden unit test coverage where playtesting finds thin spots (rings, sticks,
|
||||||
wizard commands).
|
wizard commands).
|
||||||
5. Tag a release once a full game (Amulet retrieval and score entry) completes
|
4. Tag a release once a full game (Amulet retrieval and score entry) completes
|
||||||
without defects.
|
without defects.
|
||||||
6. Full-terminal-size support (deferred by explicit decision 2026-07-06):
|
5. Full-terminal-size support (deferred by explicit decision 2026-07-06):
|
||||||
per-game dungeon dimensions instead of the 80x24 constants; open design
|
per-game dungeon dimensions instead of the 80x24 constants; open design
|
||||||
questions are resize policy, gameplay tuning at larger sizes, and a --classic
|
questions are resize policy, gameplay tuning at larger sizes, and a --classic
|
||||||
80x24 mode.
|
80x24 mode.
|
||||||
7. Note: this repo is exempt from the standard policy scaffold. A minimal dev
|
6. Note: this repo is exempt from the standard policy scaffold. A minimal dev
|
||||||
Makefile (fmt/fmt-check/lint/test/check targets) exists per sneak's
|
Makefile (fmt/fmt-check/lint/test/check targets) exists per sneak's
|
||||||
2026-07-07 request, but do not add a Dockerfile, CI config, or
|
2026-07-07 request, but do not add a Dockerfile, CI config, or
|
||||||
REPO_POLICIES.md.
|
REPO_POLICIES.md.
|
||||||
|
|||||||
@@ -21,18 +21,20 @@ func main() {
|
|||||||
os.Exit(run())
|
os.Exit(run())
|
||||||
}
|
}
|
||||||
|
|
||||||
// run carries the real main so that deferred terminal restoration runs
|
// run does the real work and returns an exit code. It only returns on a
|
||||||
// before the process exits (os.Exit skips defers).
|
// startup error; once the game starts, it ends by exiting the process
|
||||||
|
// from within (game.myExit restores the terminal first). The deferred
|
||||||
|
// Fini covers the early-return paths.
|
||||||
func run() int {
|
func run() int {
|
||||||
scores := flag.Bool("s", false, "print the scoreboard and exit")
|
scores := flag.Bool("s", false, "print the scoreboard and exit")
|
||||||
deathDemo := flag.Bool("d", false, "die a random death (demo)")
|
deathDemo := flag.Bool("d", false, "die a random death (demo)")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cfg := loadConfig()
|
params := loadParams()
|
||||||
|
|
||||||
if *scores {
|
if *scores {
|
||||||
game.NewGame(cfg).ShowScores()
|
game.New(params).ShowScores()
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -45,46 +47,39 @@ func run() int {
|
|||||||
}
|
}
|
||||||
defer t.Fini()
|
defer t.Fini()
|
||||||
|
|
||||||
cfg.Term = t
|
params.Term = t
|
||||||
|
|
||||||
var g *game.RogueGame
|
var g *game.RogueGame
|
||||||
|
|
||||||
if args := flag.Args(); len(args) == 1 && !*deathDemo {
|
if args := flag.Args(); len(args) == 1 && !*deathDemo {
|
||||||
// restore a saved game
|
// restore a saved game
|
||||||
g, err = game.Restore(args[0], cfg)
|
g, err = game.Restore(args[0], params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fini()
|
fmt.Fprintln(os.Stderr, err) // deferred Fini restores the terminal
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
g = game.NewGame(cfg)
|
g = game.New(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *deathDemo {
|
if *deathDemo {
|
||||||
g.DeathDemo()
|
g.DeathDemo() // does not return: death exits the process
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
installAutosave(g, t)
|
installAutosave(g, t)
|
||||||
|
|
||||||
runErr := g.Run()
|
g.Run() // does not return: the game ends by exiting the process
|
||||||
if runErr != nil {
|
|
||||||
t.Fini()
|
|
||||||
fmt.Fprintln(os.Stderr, runErr)
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadConfig gathers the game configuration from the environment: home
|
// loadParams gathers the game parameters from the environment: home
|
||||||
// directory, ROGUEOPTS, user name, wizard mode, and the dungeon seed
|
// directory, ROGUEOPTS, user name, wizard mode, and the dungeon seed
|
||||||
// (main.c's startup).
|
// (main.c's startup).
|
||||||
func loadConfig() game.Config {
|
func loadParams() game.Params {
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
|
|
||||||
name := ""
|
name := ""
|
||||||
@@ -96,7 +91,7 @@ func loadConfig() game.Config {
|
|||||||
|
|
||||||
wizard := os.Getenv("ROGUE_WIZARD") != ""
|
wizard := os.Getenv("ROGUE_WIZARD") != ""
|
||||||
|
|
||||||
return game.Config{
|
return game.Params{
|
||||||
Seed: chooseSeed(wizard),
|
Seed: chooseSeed(wizard),
|
||||||
Name: name,
|
Name: name,
|
||||||
RogueOpts: os.Getenv("ROGUEOPTS"),
|
RogueOpts: os.Getenv("ROGUEOPTS"),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import "testing"
|
|||||||
func mkGameInput(t *testing.T) *RogueGame {
|
func mkGameInput(t *testing.T) *RogueGame {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
g := NewGame(Config{Seed: 5, Term: &testTerm{}})
|
g := New(Params{Seed: 5, Term: &testTerm{}})
|
||||||
g.NewLevel()
|
g.NewLevel()
|
||||||
g.Oldpos = g.Player.Pos
|
g.Oldpos = g.Player.Pos
|
||||||
g.Oldrp = g.roomIn(g.Player.Pos)
|
g.Oldrp = g.roomIn(g.Player.Pos)
|
||||||
@@ -183,7 +183,7 @@ func TestZapSlowMonster(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseOpts(t *testing.T) {
|
func TestParseOpts(t *testing.T) {
|
||||||
g := NewGame(Config{Seed: 1})
|
g := New(Params{Seed: 1})
|
||||||
g.ParseOpts("terse,nojump,name=Conan,fruit=mango,inven=slow")
|
g.ParseOpts("terse,nojump,name=Conan,fruit=mango,inven=slow")
|
||||||
|
|
||||||
if !g.Options.Terse {
|
if !g.Options.Terse {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import "testing"
|
|||||||
func mkGame(t *testing.T, seed int32) *RogueGame {
|
func mkGame(t *testing.T, seed int32) *RogueGame {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
g := NewGame(Config{Seed: seed, Term: &testTerm{}})
|
g := New(Params{Seed: seed, Term: &testTerm{}})
|
||||||
g.NewLevel()
|
g.NewLevel()
|
||||||
g.Oldpos = g.Player.Pos
|
g.Oldpos = g.Player.Pos
|
||||||
g.Oldrp = g.roomIn(g.Player.Pos)
|
g.Oldrp = g.roomIn(g.Player.Pos)
|
||||||
@@ -79,24 +79,6 @@ func TestAttackHurtsPlayer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeathUnwindsWithGameEnd(t *testing.T) {
|
|
||||||
g := mkGame(t, 11)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
r := recover()
|
|
||||||
if _, ok := r.(gameEnd); !ok {
|
|
||||||
t.Fatalf("death did not unwind with gameEnd, got %v", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
if g.Playing {
|
|
||||||
t.Error("still playing after death")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
g.Options.Tombstone = false
|
|
||||||
g.death('K')
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunnersChaseHero(t *testing.T) {
|
func TestRunnersChaseHero(t *testing.T) {
|
||||||
g := mkGame(t, 3)
|
g := mkGame(t, 3)
|
||||||
// Place a hobgoblin a few squares away in the hero's room and set it
|
// Place a hobgoblin a few squares away in the hero's room and set it
|
||||||
|
|||||||
100
game/game.go
100
game/game.go
@@ -31,9 +31,9 @@ type Options struct {
|
|||||||
InvType int // inven: inventory style (InvOver/InvSlow/InvClear)
|
InvType int // inven: inventory style (InvOver/InvSlow/InvClear)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config carries everything needed to construct a game.
|
// Params carries everything needed to construct a game.
|
||||||
type Config struct {
|
type Params struct {
|
||||||
Seed int32 // dungeon number; the caller derives it (time+pid or SEED env)
|
Seed int32 // dungeon number; caller derives it (time+pid or SEED)
|
||||||
Name string // player name (overridden by ROGUEOPTS name=)
|
Name string // player name (overridden by ROGUEOPTS name=)
|
||||||
RogueOpts string // the ROGUEOPTS environment string
|
RogueOpts string // the ROGUEOPTS environment string
|
||||||
Home string // home directory (save file default location)
|
Home string // home directory (save file default location)
|
||||||
@@ -44,7 +44,7 @@ type Config struct {
|
|||||||
|
|
||||||
// RogueGame is one complete game of Rogue: every piece of state that was a
|
// RogueGame is one complete game of Rogue: every piece of state that was a
|
||||||
// global (or file-scope static) in the C sources, plus the terminal it is
|
// global (or file-scope static) in the C sources, plus the terminal it is
|
||||||
// played on. Construct with NewGame, then call Run.
|
// played on. Construct with New, then call Run.
|
||||||
//
|
//
|
||||||
// The struct grows with the port; fields appear in the phase that ports the
|
// The struct grows with the port; fields appear in the phase that ports the
|
||||||
// code owning them.
|
// code owning them.
|
||||||
@@ -143,22 +143,22 @@ type RogueGame struct {
|
|||||||
data *gameData
|
data *gameData
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGame builds a game from cfg, seeds the RNG, and randomizes the item
|
// New builds a game from params, seeds the RNG, and randomizes the item
|
||||||
// appearance tables (the front half of main.c main(); the player roll-up
|
// appearance tables (the front half of main.c main(); the player roll-up
|
||||||
// and first level arrive with later porting phases).
|
// and first level arrive with later porting phases).
|
||||||
func NewGame(cfg Config) *RogueGame {
|
func New(params Params) *RogueGame {
|
||||||
g := &RogueGame{
|
g := &RogueGame{
|
||||||
data: newGameData(),
|
data: newGameData(),
|
||||||
Rng: &Rng{Seed: cfg.Seed},
|
Rng: &Rng{Seed: params.Seed},
|
||||||
Dnum: int(cfg.Seed),
|
Dnum: int(params.Seed),
|
||||||
Whoami: cfg.Name,
|
Whoami: params.Name,
|
||||||
Fruit: "slime-mold",
|
Fruit: "slime-mold",
|
||||||
Home: cfg.Home,
|
Home: params.Home,
|
||||||
Wizard: cfg.Wizard,
|
Wizard: params.Wizard,
|
||||||
NoScore: cfg.Wizard,
|
NoScore: params.Wizard,
|
||||||
Playing: true,
|
Playing: true,
|
||||||
Depth: 1,
|
Depth: 1,
|
||||||
ScorePath: cfg.ScorePath,
|
ScorePath: params.ScorePath,
|
||||||
LastScore: -1,
|
LastScore: -1,
|
||||||
}
|
}
|
||||||
g.Options = Options{
|
g.Options = Options{
|
||||||
@@ -168,17 +168,17 @@ func NewGame(cfg Config) *RogueGame {
|
|||||||
}
|
}
|
||||||
g.InvDescribe = true
|
g.InvDescribe = true
|
||||||
g.Msgs.SaveMsg = true
|
g.Msgs.SaveMsg = true
|
||||||
g.scr = NewScreen(cfg.Term)
|
g.scr = NewScreen(params.Term)
|
||||||
g.Msgs.attach(g.scr, g.look, g.readchar)
|
g.Msgs.attach(g.scr, g.look, g.readchar)
|
||||||
g.FileName = cfg.Home + "/rogue.save"
|
g.FileName = params.Home + "/rogue.save"
|
||||||
|
|
||||||
g.rogueOpts = cfg.RogueOpts
|
g.rogueOpts = params.RogueOpts
|
||||||
if cfg.Wizard {
|
if params.Wizard {
|
||||||
g.Player.Flags.Set(SenseMonsters)
|
g.Player.Flags.Set(SenseMonsters)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.RogueOpts != "" {
|
if params.RogueOpts != "" {
|
||||||
g.ParseOpts(cfg.RogueOpts)
|
g.ParseOpts(params.RogueOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.Monsters = g.data.monsterTable
|
g.Monsters = g.data.monsterTable
|
||||||
@@ -199,37 +199,45 @@ func NewGame(cfg Config) *RogueGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run plays the game to its end: the back half of main.c main() plus
|
// Run plays the game to its end: the back half of main.c main() plus
|
||||||
// playit(). It returns after death, victory, quitting, or saving.
|
// playit(). It does not return — the game ends by exiting the process
|
||||||
func (g *RogueGame) Run() error {
|
// (see myExit); one game run is one process.
|
||||||
// A gameEnd panic is the port's my_exit(): recovering it here makes
|
func (g *RogueGame) Run() {
|
||||||
// Run return normally (zero values), restoring the terminal via the
|
g.startLevel()
|
||||||
// caller's defers.
|
g.playit()
|
||||||
defer func() {
|
}
|
||||||
if r := recover(); r != nil {
|
|
||||||
if _, ok := r.(gameEnd); ok {
|
|
||||||
return // normal game over / save exit
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(r)
|
// startLevel draws the first level and starts the standing daemons and
|
||||||
}
|
// fuses for a fresh game; a restored game brings its own (the back half
|
||||||
}()
|
// of main.c main()).
|
||||||
|
func (g *RogueGame) startLevel() {
|
||||||
if !g.restored {
|
if g.restored {
|
||||||
g.NewLevel() // draw current level
|
return
|
||||||
// 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()
|
g.NewLevel() // draw current level
|
||||||
|
// Start up daemons and fuses
|
||||||
return nil
|
g.StartDaemon(DRunners, 0, After)
|
||||||
|
g.StartDaemon(DDoctor, 0, After)
|
||||||
|
g.Fuse(DSwander, 0, wanderTime(g), After)
|
||||||
|
g.StartDaemon(DStomach, 0, After)
|
||||||
}
|
}
|
||||||
|
|
||||||
// playit is the main loop of the program (main.c playit).
|
// playit is the main loop of the program (main.c playit).
|
||||||
func (g *RogueGame) playit() {
|
func (g *RogueGame) playit() {
|
||||||
|
g.prePlay()
|
||||||
|
|
||||||
|
for g.Playing {
|
||||||
|
g.command() // command execution
|
||||||
|
}
|
||||||
|
|
||||||
|
g.endit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// prePlay does the option and position setup at the top of playit,
|
||||||
|
// before the command loop (main.c playit). It is split out so tests can
|
||||||
|
// drive a bounded number of turns; the loop itself never returns,
|
||||||
|
// because game-over exits the process.
|
||||||
|
func (g *RogueGame) prePlay() {
|
||||||
// set up defaults for modern terminals: curses' md_hasclreol() is
|
// set up defaults for modern terminals: curses' md_hasclreol() is
|
||||||
// always true, so the C default inventory style applies
|
// always true, so the C default inventory style applies
|
||||||
if !g.restored {
|
if !g.restored {
|
||||||
@@ -242,13 +250,7 @@ func (g *RogueGame) playit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.Oldpos = g.Player.Pos
|
g.Oldpos = g.Player.Pos
|
||||||
|
|
||||||
g.Oldrp = g.roomIn(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).
|
// endit exits the game (main.c endit).
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
func genLevel(t *testing.T, seed int32) *RogueGame {
|
func genLevel(t *testing.T, seed int32) *RogueGame {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
g := NewGame(Config{Seed: seed})
|
g := New(Params{Seed: seed})
|
||||||
g.NewLevel()
|
g.NewLevel()
|
||||||
|
|
||||||
return g
|
return g
|
||||||
@@ -159,7 +159,7 @@ func TestNewLevelDeterministic(t *testing.T) {
|
|||||||
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
|
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
|
||||||
func TestDeeperLevels(t *testing.T) {
|
func TestDeeperLevels(t *testing.T) {
|
||||||
for _, seed := range []int32{7, 42, 1000, 31337} {
|
for _, seed := range []int32{7, 42, 1000, 31337} {
|
||||||
g := NewGame(Config{Seed: seed})
|
g := New(Params{Seed: seed})
|
||||||
for depth := 1; depth <= 30; depth++ {
|
for depth := 1; depth <= 30; depth++ {
|
||||||
g.Depth = depth
|
g.Depth = depth
|
||||||
g.NewLevel()
|
g.NewLevel()
|
||||||
|
|||||||
30
game/rip.go
30
game/rip.go
@@ -2,23 +2,20 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rip.c — the fun ends: death or a total win.
|
// rip.c — the fun ends: death or a total win.
|
||||||
//
|
//
|
||||||
// The C functions here call exit(); the port panics with a gameEnd sentinel
|
// The C functions here call exit(). One game run is one process, so the
|
||||||
// that Run recovers, so the terminal is restored by normal unwinding.
|
// port does the same: myExit restores the terminal and exits directly.
|
||||||
|
|
||||||
// gameEnd is the sentinel carried by the panic that replaces my_exit().
|
// myExit leaves the process properly (main.c my_exit): it restores the
|
||||||
type gameEnd struct{}
|
// terminal and ends the process. Every C caller exited with status 0.
|
||||||
|
|
||||||
// myExit leaves the process properly (main.c my_exit): it unwinds to Run.
|
|
||||||
// Every C caller exited with status 0; abnormal exits panic for real.
|
|
||||||
func (g *RogueGame) myExit() {
|
func (g *RogueGame) myExit() {
|
||||||
g.Playing = false
|
g.scr.Fini()
|
||||||
|
os.Exit(0)
|
||||||
panic(gameEnd{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// death does something really fun when he dies (rip.c death).
|
// death does something really fun when he dies (rip.c death).
|
||||||
@@ -255,18 +252,9 @@ func (g *RogueGame) killname(monst byte, doart bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeathDemo implements the -d command line option (main.c): burn some
|
// DeathDemo implements the -d command line option (main.c): burn some
|
||||||
// random numbers to break patterns, then die a random death.
|
// random numbers to break patterns, then die a random death. It does not
|
||||||
|
// return — death exits the process.
|
||||||
func (g *RogueGame) DeathDemo() {
|
func (g *RogueGame) DeathDemo() {
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
if _, ok := r.(gameEnd); ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
panic(r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
dnum := g.rnd(100)
|
dnum := g.rnd(100)
|
||||||
for dnum--; dnum > 0; dnum-- {
|
for dnum--; dnum > 0; dnum-- {
|
||||||
g.rnd(100)
|
g.rnd(100)
|
||||||
|
|||||||
149
game/run_test.go
149
game/run_test.go
@@ -5,23 +5,56 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestRunScriptedSession drives a complete game through Run(): a few
|
// driveTurns runs the game's per-turn loop up to n times, doing the same
|
||||||
// moves, a rest, an inventory, then Q-quit answered yes.
|
// first-level and pre-play setup Run() does. Run() itself no longer
|
||||||
func TestRunScriptedSession(t *testing.T) {
|
// returns — game-over exits the process — so tests drive command()
|
||||||
tt := &testTerm{input: []byte("hjkl.i Qy")}
|
// directly, with short scripts that avoid quitting, saving, or playing
|
||||||
|
// long enough to starve, any of which would exit the test binary.
|
||||||
|
func driveTurns(t *testing.T, g *RogueGame, n int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
g := NewGame(Config{Seed: 99, Term: tt})
|
g.startLevel()
|
||||||
|
g.prePlay()
|
||||||
|
|
||||||
err := g.Run()
|
for range n {
|
||||||
if err != nil {
|
g.command()
|
||||||
t.Fatalf("Run: %v", err)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if g.Playing {
|
// TestRunDownStairs stands the hero on the staircase and descends via the
|
||||||
t.Error("still playing after quit")
|
// '>' command through the real turn loop, then checks the level changed.
|
||||||
|
func TestRunDownStairs(t *testing.T) {
|
||||||
|
// '>' is a free action (After=false), so it is followed by a paying
|
||||||
|
// rest ('.') to end the command() call; without a paying action the
|
||||||
|
// turn loop would spin forever on the auto-fed prompt input.
|
||||||
|
tt := &testTerm{input: []byte(">.")}
|
||||||
|
g := New(Params{Seed: 7, Term: tt})
|
||||||
|
g.NewLevel()
|
||||||
|
g.Player.Pos = g.Level.Stairs // stand on the stairs
|
||||||
|
g.restored = true // keep startLevel from regenerating
|
||||||
|
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)
|
||||||
|
|
||||||
|
driveTurns(t, g, 1)
|
||||||
|
|
||||||
|
if g.Depth != 2 {
|
||||||
|
t.Errorf("depth = %d after descending, want 2", g.Depth)
|
||||||
}
|
}
|
||||||
// After quitting, the scoreboard is the last thing shown (in C it went
|
}
|
||||||
// to stdout after endwin; here it is drawn on the screen).
|
|
||||||
|
// TestScoreRendersList checks that the scoreboard is drawn on the screen
|
||||||
|
// (in C it went to stdout after endwin; here it stays on the screen). The
|
||||||
|
// quit and death paths that normally show it now exit the process, so the
|
||||||
|
// display is exercised through score() directly.
|
||||||
|
func TestScoreRendersList(t *testing.T) {
|
||||||
|
g := New(Params{Seed: 1, Term: &testTerm{}})
|
||||||
|
g.Player.Purse = 100
|
||||||
|
|
||||||
|
g.score(g.Player.Purse, 1, 0) // flags 1 = quit; posts the top-ten list
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
|
|
||||||
for y := range NumLines {
|
for y := range NumLines {
|
||||||
@@ -31,96 +64,6 @@ func TestRunScriptedSession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
t.Error("score list not on screen after quit")
|
t.Error("score list not on screen")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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).
|
|
||||||
moves := []byte("h j k l y u b n s .")
|
|
||||||
script := make([]byte, 0, len(moves)*200+7)
|
|
||||||
|
|
||||||
for range 200 {
|
|
||||||
script = append(script, moves...)
|
|
||||||
}
|
|
||||||
|
|
||||||
script = append(script, " Q y Qy"...)
|
|
||||||
tt := &testTerm{input: script}
|
|
||||||
|
|
||||||
g := NewGame(Config{Seed: 31337, Term: tt})
|
|
||||||
|
|
||||||
err := g.Run()
|
|
||||||
if 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)
|
|
||||||
|
|
||||||
err := g.Run()
|
|
||||||
if 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
|
|
||||||
|
|
||||||
runErr := g.Run()
|
|
||||||
if runErr != nil {
|
|
||||||
t.Fatalf("Run: %v", runErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
setInput(t, h, []byte("..Qy")...)
|
|
||||||
|
|
||||||
restoredErr := h.Run()
|
|
||||||
if restoredErr != nil {
|
|
||||||
t.Fatalf("restored Run: %v", restoredErr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -681,7 +681,7 @@ var ErrSaveOutOfDate = errors.New("sorry, saved game is out of date")
|
|||||||
|
|
||||||
// Restore restores a saved game from a file (save.c restore). The file is
|
// 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.
|
// deleted, as in C, to defeat restarting from the same save.
|
||||||
func Restore(path string, cfg Config) (*RogueGame, error) {
|
func Restore(path string, params Params) (*RogueGame, error) {
|
||||||
f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design
|
f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -704,12 +704,12 @@ func Restore(path string, cfg Config) (*RogueGame, error) {
|
|||||||
data: newGameData(),
|
data: newGameData(),
|
||||||
Rng: &Rng{},
|
Rng: &Rng{},
|
||||||
Playing: true,
|
Playing: true,
|
||||||
ScorePath: cfg.ScorePath,
|
ScorePath: params.ScorePath,
|
||||||
FileName: path,
|
FileName: path,
|
||||||
rogueOpts: cfg.RogueOpts,
|
rogueOpts: params.RogueOpts,
|
||||||
restored: true,
|
restored: true,
|
||||||
}
|
}
|
||||||
g.scr = NewScreen(cfg.Term)
|
g.scr = NewScreen(params.Term)
|
||||||
g.Msgs.attach(g.scr, g.look, g.readchar)
|
g.Msgs.attach(g.scr, g.look, g.readchar)
|
||||||
g.applySnapshot(&st)
|
g.applySnapshot(&st)
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func TestSaveRestoreRoundTrip(t *testing.T) {
|
|||||||
t.Fatalf("saveFile: %v", saveErr)
|
t.Fatalf("saveFile: %v", saveErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := Restore(path, Config{Term: &testTerm{}})
|
h, err := Restore(path, Params{Term: &testTerm{}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Restore: %v", err)
|
t.Fatalf("Restore: %v", err)
|
||||||
}
|
}
|
||||||
@@ -154,7 +154,7 @@ func TestRestoreRejectsWrongVersion(t *testing.T) {
|
|||||||
t.Fatal(closeErr)
|
t.Fatal(closeErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, restoreErr := Restore(path, Config{})
|
_, restoreErr := Restore(path, Params{})
|
||||||
if restoreErr == nil {
|
if restoreErr == nil {
|
||||||
t.Error("restore accepted an out-of-date save")
|
t.Error("restore accepted an out-of-date save")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ type Terminal interface {
|
|||||||
// ReadChar blocks for the next key, translated to Rogue's input bytes
|
// ReadChar blocks for the next key, translated to Rogue's input bytes
|
||||||
// (arrows become hjkl, control keys their C0 codes).
|
// (arrows become hjkl, control keys their C0 codes).
|
||||||
ReadChar() byte
|
ReadChar() byte
|
||||||
|
// Fini restores the device to its pre-game state (curses endwin). The
|
||||||
|
// game calls it on its way out, since one game run is one process.
|
||||||
|
Fini()
|
||||||
}
|
}
|
||||||
|
|
||||||
// cell is one screen position.
|
// cell is one screen position.
|
||||||
@@ -213,6 +216,13 @@ func (s *Screen) Refresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fini restores the terminal device, if there is one (curses endwin).
|
||||||
|
func (s *Screen) Fini() {
|
||||||
|
if s.term != nil {
|
||||||
|
s.term.Fini()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshWin pushes an arbitrary window to the device (curses wrefresh).
|
// RefreshWin pushes an arbitrary window to the device (curses wrefresh).
|
||||||
func (s *Screen) RefreshWin(w *Window) {
|
func (s *Screen) RefreshWin(w *Window) {
|
||||||
if s.term != nil {
|
if s.term != nil {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func TestProbabilitiesSumTo100(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInitProbsCumulative(t *testing.T) {
|
func TestInitProbsCumulative(t *testing.T) {
|
||||||
g := NewGame(Config{Seed: 1})
|
g := New(Params{Seed: 1})
|
||||||
|
|
||||||
last := g.Items.Potions[NumPotionTypes-1].Prob
|
last := g.Items.Potions[NumPotionTypes-1].Prob
|
||||||
if last != 100 {
|
if last != 100 {
|
||||||
@@ -47,14 +47,14 @@ func TestInitProbsCumulative(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewGameRandomizesAppearances(t *testing.T) {
|
func TestNewGameRandomizesAppearances(t *testing.T) {
|
||||||
g := NewGame(Config{Seed: 12345})
|
g := New(Params{Seed: 12345})
|
||||||
|
|
||||||
checkPotionColors(t, g)
|
checkPotionColors(t, g)
|
||||||
checkScrollNames(t, g)
|
checkScrollNames(t, g)
|
||||||
checkWandMaterials(t, g)
|
checkWandMaterials(t, g)
|
||||||
|
|
||||||
// Determinism: same seed, same appearances.
|
// Determinism: same seed, same appearances.
|
||||||
h := NewGame(Config{Seed: 12345})
|
h := New(Params{Seed: 12345})
|
||||||
if h.Items != g.Items {
|
if h.Items != g.Items {
|
||||||
t.Error("two games with the same seed produced different item lore")
|
t.Error("two games with the same seed produced different item lore")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ type testTerm struct {
|
|||||||
|
|
||||||
func (t *testTerm) Render(*Window) {}
|
func (t *testTerm) Render(*Window) {}
|
||||||
|
|
||||||
|
func (t *testTerm) Fini() {}
|
||||||
|
|
||||||
func (t *testTerm) ReadChar() byte {
|
func (t *testTerm) ReadChar() byte {
|
||||||
if t.pos < len(t.input) {
|
if t.pos < len(t.input) {
|
||||||
c := t.input[t.pos]
|
c := t.input[t.pos]
|
||||||
|
|||||||
Reference in New Issue
Block a user