Compare commits
1 Commits
f0bcc7667f
...
c-master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4653c2a2e |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
*.log
|
||||
*.out
|
||||
*.test
|
||||
/rogue
|
||||
@@ -1,34 +0,0 @@
|
||||
version: "2"
|
||||
|
||||
# Config schema uses the golangci-lint v2 layout (settings live under
|
||||
# linters.settings, not top-level linters-settings) so that the
|
||||
# thresholds below are actually applied by golangci-lint >= v2.
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters:
|
||||
default: all
|
||||
disable:
|
||||
# Genuinely incompatible with project patterns
|
||||
- exhaustruct # Requires all struct fields
|
||||
- depguard # Dependency allow/block lists
|
||||
- godot # Requires comments to end with periods
|
||||
- wsl # Deprecated, replaced by wsl_v5
|
||||
- wrapcheck # Too verbose for internal packages
|
||||
- varnamelen # Short names like db, id are idiomatic Go
|
||||
settings:
|
||||
lll:
|
||||
line-length: 88
|
||||
funlen:
|
||||
lines: 80
|
||||
statements: 50
|
||||
cyclop:
|
||||
max-complexity: 15
|
||||
dupl:
|
||||
threshold: 100
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
1875
ARCHITECTURE.md
1875
ARCHITECTURE.md
File diff suppressed because it is too large
Load Diff
125
BUILD_ISSUES.md
Normal file
125
BUILD_ISSUES.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Build Issues Found During README Testing
|
||||
|
||||
This document lists issues encountered when following the README.md build instructions on macOS (darwin 25.0.0, Apple Silicon).
|
||||
|
||||
## Date
|
||||
Testing performed: Following README.md Quick Start instructions
|
||||
|
||||
## Environment
|
||||
- **OS**: macOS (darwin 25.0.0)
|
||||
- **Architecture**: Apple Silicon (arm64)
|
||||
- **Compiler**: GCC/Clang (available)
|
||||
- **make**: Available
|
||||
- **ncurses**: Installed via Homebrew at `/opt/homebrew/opt/ncurses`
|
||||
|
||||
## Issues Found
|
||||
|
||||
### 1. Compilation Error: Incomplete Type 'WINDOW'
|
||||
|
||||
**Location**: `main.c:241-242`
|
||||
|
||||
**Error Message**:
|
||||
```
|
||||
main.c:241:11: error: incomplete definition of type 'WINDOW' (aka 'struct _win_st')
|
||||
241 | curscr->_cury = oy;
|
||||
| ~~~~~~^
|
||||
main.c:242:11: error: incomplete definition of type 'WINDOW' (aka 'struct _win_st')
|
||||
242 | curscr->_curx = ox;
|
||||
| ~~~~~~^
|
||||
```
|
||||
|
||||
**Root Cause**:
|
||||
The code attempts to access internal ncurses structure members (`curscr->_cury` and `curscr->_curx`) which are not part of the public API in modern ncurses libraries. These internal members are not accessible in both the system ncurses and Homebrew ncurses.
|
||||
|
||||
**Context**:
|
||||
The problematic code is in the `tstp()` function (lines 241-242), which handles stop/start signals (SIGTSTP). After resuming from a stop signal, the code tries to manually restore the cursor position by directly accessing internal ncurses structure members.
|
||||
|
||||
**Affected Code**:
|
||||
```c
|
||||
void
|
||||
tstp(int ignored)
|
||||
{
|
||||
// ... code ...
|
||||
getyx(curscr, y, x);
|
||||
mvcur(y, x, oy, ox);
|
||||
fflush(stdout);
|
||||
curscr->_cury = oy; // ERROR: Internal member access
|
||||
curscr->_curx = ox; // ERROR: Internal member access
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- **Severity**: CRITICAL - Build fails completely
|
||||
- The game cannot be compiled on modern systems with standard ncurses
|
||||
- This affects all platforms using modern ncurses (not just macOS)
|
||||
|
||||
**Suggested Fix**:
|
||||
1. Remove the direct structure member access (lines 241-242)
|
||||
2. The `mvcur()` call on line 239 should be sufficient for cursor positioning
|
||||
3. If cursor position tracking is needed, use public ncurses API functions like `getyx()` and `move()` instead
|
||||
|
||||
**Workaround Attempted**:
|
||||
- Tried using Homebrew ncurses with explicit include/library paths as suggested in README troubleshooting section
|
||||
- Same error occurred with both system ncurses and Homebrew ncurses
|
||||
- Tried manual build using `Makefile.std` as documented in README (Alternative build method)
|
||||
- Same error occurred with manual build method
|
||||
- This confirms it's a code compatibility issue, not a configuration or build system issue
|
||||
|
||||
### 2. Compiler Warnings (Non-blocking)
|
||||
|
||||
**Location**: Multiple files
|
||||
|
||||
**Warning Messages**:
|
||||
- `main.c`: Multiple warnings about deprecated function prototypes (C23 compatibility)
|
||||
- Function declarations without prototypes in `extern.h` conflicting with definitions
|
||||
|
||||
**Impact**:
|
||||
- **Severity**: LOW - Warnings only, do not prevent compilation
|
||||
- Code compiles but may have issues with future C standards (C23)
|
||||
|
||||
**Note**: These warnings are expected for legacy C code and don't prevent the build from succeeding once the critical error is fixed.
|
||||
|
||||
## What Works
|
||||
|
||||
1. ✅ **Configure script**: Runs successfully
|
||||
- Detects compiler, headers, and libraries correctly
|
||||
- Creates `config.h` and `Makefile` properly
|
||||
- Works with both system ncurses and Homebrew ncurses paths
|
||||
|
||||
2. ✅ **Prerequisites**: All required tools are available
|
||||
- C compiler (gcc/clang)
|
||||
- make
|
||||
- ncurses library (via Homebrew)
|
||||
|
||||
3. ✅ **Build system**: Autotools configuration works correctly
|
||||
- `configure` script exists and is executable
|
||||
- No need for `autoreconf` (as documented in README)
|
||||
|
||||
## Recommendations for README Updates
|
||||
|
||||
1. **Add Known Compatibility Issue Section**:
|
||||
- Document that the codebase has compatibility issues with modern ncurses
|
||||
- Note that direct access to internal ncurses structures (`curscr->_cury`, `curscr->_curx`) will fail on modern systems
|
||||
- Provide information about which ncurses versions are known to work (if any)
|
||||
|
||||
2. **Update macOS Troubleshooting**:
|
||||
- The current macOS troubleshooting section mentions Homebrew ncurses paths, but this doesn't solve the compilation error
|
||||
- Add a note that even with correct ncurses paths, the build may fail due to code compatibility issues
|
||||
|
||||
3. **Add Build Status**:
|
||||
- Consider adding a "Known Issues" or "Build Status" section to the README
|
||||
- Indicate that the codebase may need patches for modern ncurses compatibility
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Code Fix Required**: The `main.c` file needs to be updated to remove direct access to internal ncurses structure members
|
||||
2. **Testing**: After fix, verify the game builds and runs correctly
|
||||
3. **Documentation**: Update README with any workarounds or fixes applied
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- The README's Quick Start instructions are clear and easy to follow
|
||||
- The build system (Autotools) is properly configured
|
||||
- The issue is specifically with code compatibility, not with the build system or documentation
|
||||
- This appears to be a known issue with porting old curses-based code to modern ncurses
|
||||
|
||||
88
MEMORY.md
88
MEMORY.md
@@ -1,88 +0,0 @@
|
||||
# Project Memory
|
||||
|
||||
Working notes for agents on this repo. Read this alongside TODO.md (which holds
|
||||
the step queue and workflow) before starting work.
|
||||
|
||||
## Error handling
|
||||
|
||||
Panicking on bad/unexpected errors is allowed and preferred over threading
|
||||
unlikely error returns through game code — e.g. write-side Close/encode failures
|
||||
where continuing would mean corrupt state. Return errors where a caller
|
||||
genuinely handles them (save-file prompts, restore validation). Reserve
|
||||
deliberate `_ =` discards for true best-effort paths (scorefile writes,
|
||||
`Terminal.Interrupt`'s post to a full event queue), always with a comment saying
|
||||
why.
|
||||
|
||||
Signal-time autosave used to be on that list and no longer is (issue #24). It is
|
||||
best effort in the sense that nothing can be reported to a player whose terminal
|
||||
is already going away, but the outcome is a value, not a discard: the signal
|
||||
goroutine calls `AutoSaveOnSignal`, which hands the save to the game goroutine —
|
||||
the only one allowed to touch game state — and returns whether it was taken
|
||||
before the deadline. The game answers between turns (`command`), while parked
|
||||
waiting for a key (`readchar`), and while parked in the `!` shell escape
|
||||
(`runShellEscape`). `saveFile` writes a temporary file and renames it over the
|
||||
target, so a save that fails or never happens leaves the player's previous save
|
||||
whole; never reintroduce a `Remove` before the write in `autoSave`, and never
|
||||
encode game state from any goroutine but the game's.
|
||||
|
||||
Do not upgrade that into "the snapshot is always taken between commands" — it is
|
||||
not. What is true is that the encode runs on the state-owning goroutine, so the
|
||||
snapshot is internally consistent and restorable. Only the check at the top of
|
||||
`command` is a between-commands snapshot; the other two service points both sit
|
||||
inside a `command` call already under way. `readchar` is reached from
|
||||
mid-command prompts (`--More--`, `askOverwrite`, `getStr`, direction and pack
|
||||
prompts) with the command's mutations already applied, and `runShellEscape` is
|
||||
reached from `shell`, an ordinary `'!'` command handler, with that turn's
|
||||
`DoDaemons(Before)`/`DoFuses(Before)` already fired and its AFTER pass not yet.
|
||||
Restoring re-enters `playit` at the top of `command`, so either way the rest of
|
||||
that command is lost and a fresh BEFORE pass runs on top of the one already in
|
||||
the snapshot. That is acceptable and documented; two successive false claims —
|
||||
first that `readchar` was safe, then that two of the three service points were
|
||||
between-commands — were caught in review of PR #26, and neither may come back.
|
||||
|
||||
Related, and easy to reintroduce: work moved onto a helper goroutine must not be
|
||||
allowed to panic there. A panic at the top of any goroutine kills the process
|
||||
without running the other goroutines' defers, including `cmd/rogue/main.go`'s
|
||||
`defer t.Fini()`, which is what leaves a raw tty (issue #12). `runShellEscape`
|
||||
recovers its helper's panic and re-raises it on the game goroutine for exactly
|
||||
that reason.
|
||||
|
||||
C's exit() calls are not unwound: one game run is one process, so myExit
|
||||
(game/rip.go) restores the terminal via Terminal.Fini and calls os.Exit(0), and
|
||||
Run() never returns. There is nothing to recover — do not write code that
|
||||
expects to regain control after game-over. The testing consequence is that any
|
||||
death (combat, starvation, level drain, freezing) exits the _test binary_, so
|
||||
tests drive command() directly rather than Run(), and crash-sweep drives pin the
|
||||
hero each turn with the fortify() helper in game/run_test.go.
|
||||
|
||||
## Linting
|
||||
|
||||
The .golangci.yml is byte-identical to the canonical shared config and must not
|
||||
be edited — not even to add an exception. To disable a linter, ask sneak,
|
||||
explaining what the linter does; approved exceptions are recorded as in-code
|
||||
//nolint directives (file-level where a whole file is affected) carrying the
|
||||
approval date, which is what keeps the config canonical. Approved so far:
|
||||
testpackage, exhaustive, and mnd (2026-07-07). paralleltest was approved on
|
||||
2026-07-06 but the exception is no longer in force — it was fixed instead, with
|
||||
t.Parallel() in all 32 tests. The complexity linters (cyclop, gocognit, nestif)
|
||||
are enabled and clean as of refactor step 7 (2026-07-07): the whole
|
||||
golangci-lint run is 0 issues, so keep it that way — decompose new hot spots
|
||||
rather than reaching for a nolint. Line-level //nolint with a reason is used
|
||||
sparingly for C-faithfulness (e.g. the authentic "missle" message spellings) and
|
||||
provably-safe gosec conversions; each needs a justifying comment.
|
||||
|
||||
## Faithfulness
|
||||
|
||||
Behavior must not change during the idiomatic-Go refactor unless a TODO step
|
||||
says so. The 80x24 seed-compatible gameplay, message text (including original
|
||||
typos), RNG call order, and C quirks (documented in tests like
|
||||
TestHoldScrollGreedyMonsterQuirk) are contract. Doc comments keep their "(file.c
|
||||
func_name)" breadcrumbs.
|
||||
|
||||
## Debugging
|
||||
|
||||
Write real, committed test files with t.Logf output and run them with the make
|
||||
targets — `make test` (or `make check` for the full gate); never raw `go test`.
|
||||
The target carries `-timeout 30s -race -cover` and reruns verbosely on failure,
|
||||
so a raw invocation silently drops the race detector. No throwaway scratch
|
||||
scripts. Successful debug probes become regression tests.
|
||||
39
Makefile
39
Makefile
@@ -1,39 +0,0 @@
|
||||
# Development convenience targets. This repo is exempt from the standard
|
||||
# policy scaffold (no Dockerfile, CI, or REPO_POLICIES.md); this Makefile
|
||||
# is only a thin wrapper around the Go toolchain, golangci-lint, and
|
||||
# prettier so `make fmt` / `make check` behave the same as in sneak's
|
||||
# other repos.
|
||||
|
||||
GO_PKGS := ./...
|
||||
MD_FILES := $(shell git ls-files '*.md')
|
||||
PRETTIER := prettier --tab-width 4 --prose-wrap always
|
||||
|
||||
.PHONY: check fmt fmt-check lint test
|
||||
|
||||
# Format, lint, and test — the full local pre-commit gate.
|
||||
check: fmt-check lint test
|
||||
|
||||
# Format Go and Markdown in place.
|
||||
fmt:
|
||||
gofmt -w .
|
||||
$(PRETTIER) --write $(MD_FILES)
|
||||
|
||||
# Fail if any Go or Markdown file is not formatted.
|
||||
fmt-check:
|
||||
@unformatted="$$(gofmt -l .)"; \
|
||||
if [ -n "$$unformatted" ]; then \
|
||||
echo "gofmt needed on:"; echo "$$unformatted"; exit 1; \
|
||||
fi
|
||||
$(PRETTIER) --check $(MD_FILES)
|
||||
|
||||
# Run the house linter (config in .golangci.yml).
|
||||
lint:
|
||||
golangci-lint run $(GO_PKGS)
|
||||
|
||||
# Run the test suite. Quiet on success; on failure, rerun verbosely for the
|
||||
# full output and still fail the target (the first run already proved the
|
||||
# tests are broken, so a flaky pass on the rerun must not rescue the build).
|
||||
test:
|
||||
@go test -timeout 30s -race -cover $(GO_PKGS) || \
|
||||
{ echo "--- Rerunning with -v for details ---"; \
|
||||
go test -timeout 30s -race -v $(GO_PKGS); exit 1; }
|
||||
220
Makefile.in
Normal file
220
Makefile.in
Normal file
@@ -0,0 +1,220 @@
|
||||
###############################################################################
|
||||
#
|
||||
# Makefile for rogue
|
||||
#
|
||||
# Rogue: Exploring the Dungeons of Doom
|
||||
# Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
# All rights reserved.
|
||||
#
|
||||
# See the file LICENSE.TXT for full copyright and licensing information.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
# Site configuration occurs beneath this comment
|
||||
# Typically ./configure (autoconf tools) configures this section
|
||||
# This section could be manually configured if autoconf/configure fails
|
||||
###############################################################################
|
||||
|
||||
DISTNAME=@PACKAGE_TARNAME@@PACKAGE_VERSION@
|
||||
PACKAGE_TARNAME = @PACKAGE_TARNAME@-@PACKAGE_VERSION@
|
||||
PROGRAM=@PROGRAM@
|
||||
|
||||
O=o
|
||||
|
||||
#CC=gcc
|
||||
CC = @CC@
|
||||
|
||||
#CFLAGS=-O2
|
||||
CFLAGS= @CFLAGS@
|
||||
|
||||
#LIBS=-lcurses
|
||||
LIBS = @LIBS@
|
||||
|
||||
#RM=rm -f
|
||||
RM = rm -f
|
||||
|
||||
#GROFF=groff
|
||||
GROFF = @GROFF@
|
||||
|
||||
#NROFF=nroff
|
||||
NROFF = @NROFF@
|
||||
|
||||
#TBL=tbl
|
||||
TBL = @TBL@
|
||||
|
||||
#COLCRT=colcrt
|
||||
COLCRT = @COLCRT@
|
||||
|
||||
#SED=sed
|
||||
SED = @SED@
|
||||
|
||||
#SCOREFILE=rogue54.scr
|
||||
SCOREFILE = @SCOREFILE@
|
||||
|
||||
#LOCKFILE=rogue54.lck
|
||||
LOCKFILE = @LOCKFILE@
|
||||
|
||||
#GROUPOWNER=games
|
||||
GROUPOWNER = @GROUPOWNER@
|
||||
|
||||
#CPPFLAGS=-DHAVE_CONFIG_H
|
||||
CPPFLAGS =@DEFS@ @CPPFLAGS@
|
||||
|
||||
#DISTFILE = $(PROGRAM)
|
||||
DISTFILE = $(DISTNAME)-@TARGET@
|
||||
|
||||
INSTALL=./install-sh
|
||||
|
||||
#INSTGROUP=-g games
|
||||
INSTGROUP=
|
||||
#INSTOWNER=-u root
|
||||
INSTOWNER=
|
||||
|
||||
CHGRP=chgrp
|
||||
|
||||
MKDIR=mkdir
|
||||
|
||||
TOUCH=touch
|
||||
|
||||
RMDIR=rmdir
|
||||
|
||||
CHMOD=chmod
|
||||
|
||||
DESTDIR=
|
||||
|
||||
prefix=@prefix@
|
||||
exec_prefix=@exec_prefix@
|
||||
datarootdir=@datarootdir@
|
||||
datadir=@datadir@
|
||||
bindir=@bindir@
|
||||
mandir=@mandir@
|
||||
docdir=@docdir@
|
||||
man6dir = $(mandir)/man6
|
||||
|
||||
###############################################################################
|
||||
# Site configuration occurs above this comment
|
||||
# It should not be necessary to change anything below this comment
|
||||
###############################################################################
|
||||
|
||||
HDRS = rogue.h extern.h score.h
|
||||
OBJS1 = vers.$(O) extern.$(O) armor.$(O) chase.$(O) command.$(O) \
|
||||
daemon.$(O) daemons.$(O) fight.$(O) init.$(O) io.$(O) list.$(O) \
|
||||
mach_dep.$(O) main.$(O) mdport.$(O) misc.$(O) monsters.$(O) \
|
||||
move.$(O) new_level.$(O)
|
||||
OBJS2 = options.$(O) pack.$(O) passages.$(O) potions.$(O) rings.$(O) \
|
||||
rip.$(O) rooms.$(O) save.$(O) scrolls.$(O) state.$(O) sticks.$(O) \
|
||||
things.$(O) weapons.$(O) wizard.$(O) xcrypt.$(O)
|
||||
OBJS = $(OBJS1) $(OBJS2)
|
||||
CFILES = vers.c extern.c armor.c chase.c command.c daemon.c \
|
||||
daemons.c fight.c init.c io.c list.c mach_dep.c \
|
||||
main.c mdport.c misc.c monsters.c move.c new_level.c \
|
||||
options.c pack.c passages.c potions.c rings.c rip.c \
|
||||
rooms.c save.c scrolls.c state.c sticks.c things.c \
|
||||
weapons.c wizard.c xcrypt.c
|
||||
MISC_C = findpw.c scedit.c scmisc.c
|
||||
DOCSRC = rogue.me.in rogue.6.in rogue.doc.in rogue.html.in rogue.cat.in
|
||||
DOCS = $(PROGRAM).doc $(PROGRAM).html $(PROGRAM).cat $(PROGRAM).me \
|
||||
$(PROGRAM).6
|
||||
AFILES = configure Makefile.in configure.ac config.h.in config.sub config.guess \
|
||||
install-sh rogue.6.in rogue.me.in rogue.html.in rogue.doc.in rogue.cat.in
|
||||
MISC = Makefile.std LICENSE.TXT rogue54.sln rogue54.vcproj rogue.spec \
|
||||
rogue.png rogue.desktop
|
||||
|
||||
.SUFFIXES: .obj
|
||||
|
||||
.c.obj:
|
||||
$(CC) $(CFLAGS) $(CPPFLAGS) /c $*.c
|
||||
|
||||
.c.o:
|
||||
$(CC) $(CFLAGS) $(CPPFLAGS) -c $*.c
|
||||
|
||||
$(PROGRAM): $(HDRS) $(OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
clean:
|
||||
$(RM) $(OBJS1)
|
||||
$(RM) $(OBJS2)
|
||||
$(RM) core a.exe a.out a.exe.stackdump $(PROGRAM) $(PROGRAM).exe
|
||||
$(RM) $(PROGRAM).tar $(PROGRAM).tar.gz $(PROGRAM).zip
|
||||
$(RM) $(DISTNAME)/*
|
||||
-rmdir $(DISTNAME)
|
||||
|
||||
maintainer-clean:
|
||||
$(RM) config.h
|
||||
$(RM) Makefile
|
||||
$(RM) config.status
|
||||
$(RM) -r autom4te.cache
|
||||
$(RM) config.log
|
||||
$(RM) $(PROGRAM).scr $(PROGRAM).lck
|
||||
|
||||
stddocs:
|
||||
sed -e 's/@PROGRAM@/rogue/' -e 's/@SCOREFILE@/rogue.scr/' rogue.6.in > rogue.6
|
||||
sed -e 's/@PROGRAM@/rogue/' -e 's/@SCOREFILE@/rogue.scr/' rogue.me.in > rogue.me
|
||||
sed -e 's/@PROGRAM@/rogue/' -e 's/@SCOREFILE@/rogue.scr/' rogue.html.in > rogue,html
|
||||
sed -e 's/@PROGRAM@/rogue/' -e 's/@SCOREFILE@/rogue.scr/' rogue.doc.in > rogue.doc
|
||||
sed -e 's/@PROGRAM@/rogue/' -e 's/@SCOREFILE@/rogue.scr/' rogue.cat.in > rogue.cat
|
||||
|
||||
dist.src:
|
||||
$(MAKE) $(MAKEFILE) clean
|
||||
mkdir $(DISTNAME)
|
||||
cp $(CFILES) $(HDRS) $(MISC) $(AFILES) $(DISTNAME)
|
||||
tar cf $(DISTNAME)-src.tar $(DISTNAME)
|
||||
gzip -f $(DISTNAME)-src.tar
|
||||
rm -fr $(DISTNAME)
|
||||
|
||||
findpw: findpw.c xcrypt.o mdport.o xcrypt.o
|
||||
$(CC) -s -o findpw findpw.c xcrypt.o mdport.o -lcurses
|
||||
|
||||
scedit: scedit.o scmisc.o vers.o mdport.o xcrypt.o
|
||||
$(CC) -s -o scedit vers.o scedit.o scmisc.o mdport.o xcrypt.o -lcurses
|
||||
|
||||
scmisc.o scedit.o:
|
||||
$(CC) -O -c $(SF) $*.c
|
||||
|
||||
$(PROGRAM).doc: rogue.me
|
||||
if test "x$(GROFF)" != "x" -a "x$(SED)" != "x" ; then \
|
||||
$(GROFF) -P-c -t -me -Tascii rogue.me | $(SED) -e 's/.\x08//g' > $(PROGRAM).doc ;\
|
||||
elif test "x$(NROFF)" != "x" -a "x$(TBL)" != "x" -a "x$(COLCRT)" != "x" ; then \
|
||||
tbl rogue.me | $(NROFF) -me | colcrt - > $(PROGRAM).doc ;\
|
||||
fi
|
||||
|
||||
$(PROGRAM).cat: rogue.6
|
||||
if test "x$(GROFF)" != "x" -a "x$(SED)" != "x" ; then \
|
||||
$(GROFF) -Tascii -man rogue.6 | $(SED) -e 's/.\x08//g' > $(PROGRAM).cat ;\
|
||||
elif test "x$(NROFF)" != "x" -a "x$(TBL)" != "x" -a "x$(COLCRT)" != "x" ; then \
|
||||
$(NROFF) -man rogue.6 | $(COLCRT) - > $(PROGRAM).cat ;\
|
||||
fi
|
||||
|
||||
dist: clean $(PROGRAM)
|
||||
tar cf $(DISTFILE).tar $(PROGRAM) LICENSE.TXT $(DOCS)
|
||||
gzip -f $(DISTFILE).tar
|
||||
|
||||
install: $(PROGRAM)
|
||||
-$(TOUCH) test
|
||||
-if test ! -f $(DESTDIR)$(SCOREFILE) ; then $(INSTALL) -m 0664 test $(DESTDIR)$(SCOREFILE) ; fi
|
||||
-$(INSTALL) -m 0755 $(PROGRAM) $(DESTDIR)$(bindir)/$(PROGRAM)
|
||||
-if test "x$(GROUPOWNER)" != "x" ; then \
|
||||
$(CHGRP) $(GROUPOWNER) $(DESTDIR)$(SCOREFILE) ; \
|
||||
$(CHGRP) $(GROUPOWNER) $(DESTDIR)$(bindir)/$(PROGRAM) ; \
|
||||
$(CHMOD) 02755 $(DESTDIR)$(bindir)/$(PROGRAM) ; \
|
||||
$(CHMOD) 0464 $(DESTDIR)$(SCOREFILE) ; \
|
||||
fi
|
||||
-if test -d $(man6dir) ; then $(INSTALL) -m 0644 rogue.6 $(DESTDIR)$(man6dir)/$(PROGRAM).6 ; fi
|
||||
-if test ! -d $(man6dir) ; then $(INSTALL) -m 0644 rogue.6 $(DESTDIR)$(mandir)/$(PROGRAM).6 ; fi
|
||||
-$(INSTALL) -m 0644 rogue.doc $(DESTDIR)$(docdir)/$(PROGRAM).doc
|
||||
-$(INSTALL) -m 0644 rogue.html $(DESTDIR)$(docdir)/$(PROGRAM).html
|
||||
-$(INSTALL) -m 0644 rogue.cat $(DESTDIR)$(docdir)/$(PROGRAM).cat
|
||||
-$(INSTALL) -m 0644 LICENSE.TXT $(DESTDIR)$(docdir)/LICENSE.TXT
|
||||
-$(INSTALL) -m 0644 rogue.me $(DESTDIR)$(docdir)/$(PROGRAM).me
|
||||
-if test ! -f $(DESTDIR)$(LOCKFILE) ; then $(INSTALL) -m 0666 test $(DESTDIR)$(LOCKFILE) ; $(RM) $(DESTDIR)$(LOCKFILE) ; fi
|
||||
-$(RM) test
|
||||
|
||||
uninstall:
|
||||
-$(RM) $(DESTDIR)$(bindir)/$(PROGRAM)
|
||||
-$(RM) $(DESTDIR)$(man6dir)/$(PROGRAM).6
|
||||
-$(RM) $(DESTDIR)$(docdir)$(PROGRAM)/$(PROGRAM).doc
|
||||
-$(RM) $(DESTDIR)$(LOCKFILE)
|
||||
-$(RMDIR) $(DESTDIR)$(docdir)$(PROGRAM)
|
||||
|
||||
reinstall: uninstall install
|
||||
158
Makefile.std
Executable file
158
Makefile.std
Executable file
@@ -0,0 +1,158 @@
|
||||
#
|
||||
# Makefile for rogue
|
||||
# @(#)Makefile 4.21 (Berkeley) 02/04/99
|
||||
#
|
||||
# Rogue: Exploring the Dungeons of Doom
|
||||
# Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
# All rights reserved.
|
||||
#
|
||||
# See the file LICENSE.TXT for full copyright and licensing information.
|
||||
#
|
||||
|
||||
DISTNAME = rogue5.4.4
|
||||
PROGRAM = rogue54
|
||||
O = o
|
||||
HDRS = rogue.h extern.h score.h
|
||||
OBJS1 = vers.$(O) extern.$(O) armor.$(O) chase.$(O) command.$(O) \
|
||||
daemon.$(O) daemons.$(O) fight.$(O) init.$(O) io.$(O) list.$(O) \
|
||||
mach_dep.$(O) main.$(O) mdport.$(O) misc.$(O) monsters.$(O) \
|
||||
move.$(O) new_level.$(O)
|
||||
OBJS2 = options.$(O) pack.$(O) passages.$(O) potions.$(O) rings.$(O) \
|
||||
rip.$(O) rooms.$(O) save.$(O) scrolls.$(O) state.$(O) sticks.$(O) \
|
||||
things.$(O) weapons.$(O) wizard.$(O) xcrypt.$(O)
|
||||
OBJS = $(OBJS1) $(OBJS2)
|
||||
CFILES = vers.c extern.c armor.c chase.c command.c daemon.c \
|
||||
daemons.c fight.c init.c io.c list.c mach_dep.c \
|
||||
main.c mdport.c misc.c monsters.c move.c new_level.c \
|
||||
options.c pack.c passages.c potions.c rings.c rip.c \
|
||||
rooms.c save.c scrolls.c state.c sticks.c things.c \
|
||||
weapons.c wizard.c xcrypt.c
|
||||
MISC_C = findpw.c scedit.c scmisc.c
|
||||
DOCSRC = rogue.me.in rogue.6.in rogue.doc.in rogue.html.in rogue.cat.in
|
||||
DOCS = $(PROGRAM).doc $(PROGRAM).html $(PROGRAM).cat $(PROGRAM).me \
|
||||
$(PROGRAM).6
|
||||
AFILES = configure Makefile.in configure.ac config.h.in config.sub config.guess \
|
||||
install-sh rogue.6.in rogue.me.in rogue.html.in rogue.doc.in rogue.cat.in
|
||||
MISC = Makefile.std LICENSE.TXT rogue54.sln rogue54.vcproj rogue.spec \
|
||||
rogue.png rogue.desktop
|
||||
CC = gcc
|
||||
FEATURES = -DALLSCORES -DSCOREFILE=\"$(SCOREFILE)\" -DLOCKFILE=\"$(LOCKFILE)\"
|
||||
CPPFLAGS =
|
||||
CFLAGS = -O3
|
||||
LDFLAGS =
|
||||
LIBS = -lcurses
|
||||
RM = rm -f
|
||||
MAKEFILE = -f Makefile.std
|
||||
SCOREFILE= $(PROGRAM).scr
|
||||
LOCKFILE = $(PROGRAM).lck
|
||||
OUTFLAG = -o
|
||||
EXE =
|
||||
|
||||
.SUFFIXES: .obj
|
||||
|
||||
.c.obj:
|
||||
$(CC) $(CFLAGS) $(CPPFLAGS) $(FEATURES) /c $*.c
|
||||
|
||||
.c.o:
|
||||
$(CC) $(CFLAGS) $(CPPFLAGS) $(FEATURES) -c $*.c
|
||||
|
||||
$(PROGRAM): $(HDRS) $(OBJS) fixdocs
|
||||
$(CC) $(LDFLAGS) $(OBJS) $(LIBS) $(OUTFLAG)$@$(EXE)
|
||||
|
||||
clean:
|
||||
$(RM) $(OBJS1)
|
||||
$(RM) $(OBJS2)
|
||||
$(RM) core a.exe a.out a.exe.stackdump $(PROGRAM) $(PROGRAM).exe $(PROGRAM).lck
|
||||
$(RM) $(PROGRAM).tar $(PROGRAM).tar.gz $(PROGRAM).zip
|
||||
$(RM) $(DISTNAME)/*
|
||||
|
||||
dist.src:
|
||||
$(MAKE) $(MAKEFILE) clean
|
||||
mkdir $(DISTNAME)
|
||||
cp $(CFILES) $(HDRS) $(MISC) $(AFILES) $(DISTNAME)
|
||||
tar cf $(DISTNAME)-src.tar $(DISTNAME)
|
||||
gzip -f $(DISTNAME)-src.tar
|
||||
rm -fr $(DISTNAME)
|
||||
|
||||
findpw: findpw.c xcrypt.o mdport.o xcrypt.o
|
||||
$(CC) -s -o findpw findpw.c xcrypt.o mdport.o -lcurses
|
||||
|
||||
scedit: scedit.o scmisc.o vers.o mdport.o xcrypt.o
|
||||
$(CC) -s -o scedit vers.o scedit.o scmisc.o mdport.o xcrypt.o -lcurses
|
||||
|
||||
scmisc.o scedit.o:
|
||||
$(CC) -O -c $(SF) $*.c
|
||||
|
||||
doc.nroff:
|
||||
tbl rogue.me | nroff -me | colcrt - > rogue.doc
|
||||
nroff -man rogue.6 | colcrt - > rogue.cat
|
||||
|
||||
doc.groff:
|
||||
groff -P-c -t -me -Tascii rogue.me | sed -e 's/.\x08//g' > rogue.doc
|
||||
groff -man rogue.6 | sed -e 's/.\x08//g' > rogue.cat
|
||||
|
||||
fixdocs:
|
||||
sed -e 's/@PROGRAM@/$(PROGRAM)/' -e 's/@SCOREFILE@/$(SCOREFILE)/' rogue.6.in > $(PROGRAM).6
|
||||
sed -e 's/@PROGRAM@/$(PROGRAM)/' -e 's/@SCOREFILE@/$(SCOREFILE)/' rogue.me.in > $(PROGRAM).me
|
||||
sed -e 's/@PROGRAM@/$(PROGRAM)/' -e 's/@SCOREFILE@/$(SCOREFILE)/' rogue.html.in > $(PROGRAM).html
|
||||
sed -e 's/@PROGRAM@/$(PROGRAM)/' -e 's/@SCOREFILE@/$(SCOREFILE)/' rogue.doc.in > $(PROGRAM).doc
|
||||
sed -e 's/@PROGRAM@/$(PROGRAM)/' -e 's/@SCOREFILE@/$(SCOREFILE)/' rogue.cat.in > $(PROGRAM).cat
|
||||
|
||||
dist.irix:
|
||||
$(MAKE) $(MAKEFILE) clean
|
||||
$(MAKE) $(MAKEFILE) CC=cc $(PROGRAM)
|
||||
tar cf $(DISTNAME)-irix.tar $(PROGRAM) LICENSE.TXT $(DOCS)
|
||||
gzip -f $(DISTNAME)-irix.tar
|
||||
|
||||
dist.aix:
|
||||
$(MAKE) $(MAKEFILE) clean
|
||||
$(MAKE) $(MAKEFILE) CC=xlc CFLAGS="-qmaxmem=16768 -O3 -qstrict" $(PROGRAM)
|
||||
tar cf $(DISTNAME)-aix.tar $(PROGRAM) LICENSE.TXT $(DOCS)
|
||||
gzip -f $(DISTNAME)-aix.tar
|
||||
|
||||
dist.linux:
|
||||
$(MAKE) $(MAKEFILE) clean
|
||||
$(MAKE) $(MAKEFILE) $(PROGRAM)
|
||||
tar cf $(DISTNAME)-linux.tar $(PROGRAM) LICENSE.TXT $(DOCS)
|
||||
gzip -f $(DISTNAME)-linux.tar
|
||||
|
||||
dist.interix:
|
||||
@$(MAKE) $(MAKEFILE) clean
|
||||
@$(MAKE) $(MAKEFILE) CFLAGS="-ansi" $(PROGRAM)
|
||||
tar cf $(DISTNAME)-interix.tar $(PROGRAM) LICENSE.TXT $(DOCS)
|
||||
gzip -f $(DISTNAME)-interix.tar
|
||||
|
||||
dist.cygwin:
|
||||
@$(MAKE) $(MAKEFILE) --no-print-directory clean
|
||||
@$(MAKE) $(MAKEFILE) CPPFLAGS="-I/usr/include/ncurses" --no-print-directory $(PROGRAM)
|
||||
tar cf $(DISTNAME)-cygwin.tar $(PROGRAM).exe LICENSE.TXT $(DOCS)
|
||||
gzip -f $(DISTNAME)-cygwin.tar
|
||||
|
||||
#
|
||||
# Use MINGW32-MAKE to build this target
|
||||
#
|
||||
dist.mingw32:
|
||||
@$(MAKE) $(MAKEFILE) --no-print-directory RM="cmd /c del" clean
|
||||
@$(MAKE) $(MAKEFILE) --no-print-directory CPPFLAGS="-I../pdcurses" LIBS="../pdcurses/pdcurses.a" $(PROGRAM)
|
||||
cmd /c del $(DISTNAME)-mingw32.zip
|
||||
zip $(DISTNAME)-mingw32.zip $(PROGRAM).exe LICENSE.TXT $(DOCS)
|
||||
|
||||
dist.djgpp:
|
||||
@$(MAKE) $(MAKEFILE) --no-print-directory clean
|
||||
@$(MAKE) $(MAKEFILE) --no-print-directory LDFLAGS="-L$(DJDIR)/LIB" \
|
||||
LIBS="-lpdcurses" $(PROGRAM)
|
||||
rm -f $(DISTNAME)-djgpp.zip
|
||||
zip $(DISTNAME)-djgpp.zip $(PROGRAM) LICENSE.TXT $(DOCS)
|
||||
|
||||
#
|
||||
# Use NMAKE to build this targer
|
||||
#
|
||||
|
||||
dist.win32:
|
||||
@$(MAKE) $(MAKEFILE) /NOLOGO O="obj" RM="-del" clean
|
||||
@$(MAKE) $(MAKEFILE) /NOLOGO O="obj" CC="CL" \
|
||||
LIBS="..\pdcurses\pdcurses.lib shell32.lib user32.lib Advapi32.lib" \
|
||||
EXE=".exe" OUTFLAG="/Fe" CPPFLAGS="-I..\pdcurses" \
|
||||
CFLAGS="-nologo -Ox -wd4033 -wd4716" $(PROGRAM)
|
||||
-del $(DISTNAME)-win32.zip
|
||||
zip $(DISTNAME)-win32.zip $(PROGRAM).exe LICENSE.TXT $(DOCS)
|
||||
716
README.md
716
README.md
@@ -1,90 +1,702 @@
|
||||
# Rogue: Exploring the Dungeons of Doom (Go port)
|
||||
# Rogue: Exploring the Dungeons of Doom
|
||||
|
||||
[](LICENSE.TXT)
|
||||
|
||||
**Rogue** is the original dungeon-crawling adventure game that spawned an entire
|
||||
genre. This branch is a faithful Go port of Rogue 5.4.4: explore procedurally
|
||||
generated dungeons, fight monsters, collect treasure, and attempt to retrieve
|
||||
the Amulet of Yendor.
|
||||
**Rogue** is the original graphical dungeon-crawling adventure game that spawned an entire genre. This is version 5.4.4, a classic roguelike where you explore procedurally generated dungeons, fight monsters, collect treasure, and attempt to retrieve the Amulet of Yendor.
|
||||
|
||||
**Original authors:** Michael Toy, Ken Arnold, and Glenn Wichman (1980–1983,
|
||||
1985, 1999).
|
||||
**Original Authors:** Michael Toy, Ken Arnold, and Glenn Wichman (1980-1983, 1985, 1999)
|
||||
|
||||
The port is function-by-function faithful to the classic C sources — same
|
||||
dungeon generation (seed-compatible RNG), same combat math, same item tables,
|
||||
same messages. The C reference implementation lives on the `master` and
|
||||
`modern-rogue` branches; [ARCHITECTURE.md](ARCHITECTURE.md) documents both the
|
||||
original program structure and the design of this port.
|
||||
---
|
||||
|
||||
## Building and running
|
||||
## ⚠️ About This Repository ⚠️
|
||||
|
||||
Requires Go 1.25 or later and a terminal at least 80x24.
|
||||
**November 2025**:
|
||||
|
||||
I want to express my sincere appreciation to everyone who has contributed to this repository, attempted to fix issues, and forked the project over the years. When I originally created this repository in [July 2016](https://github.com/Davidslv/rogue/releases/tag/5.4.4), I was young and fascinated by this classic game. My primary intention was to archive the codebase for learning purposes, and I never expected the community engagement that followed.
|
||||
|
||||
**Important**: I am not one of the original authors of Rogue. I am simply maintaining this repository as an archive of the original game.
|
||||
|
||||
**Repository Policy**:
|
||||
- The **`main` branch** will remain unchanged to preserve the game exactly as it was in 1999. This ensures the original codebase remains available in its historical state.
|
||||
- For modernization efforts, bug fixes, and improvements, please use the branch: **[modern-rogue](https://github.com/Davidslv/rogue/tree/modern-rogue)**
|
||||
- You are welcome to fork this repository and make your own modifications. Many have done so over the years, and I encourage continued development in your own forks.
|
||||
|
||||
Thank you for your interest in preserving and improving this classic game! ❤️
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Building from Source](#building-from-source)
|
||||
- [Running the Game](#running-the-game)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Development Guide](#development-guide)
|
||||
- [Contributing](#contributing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Resources](#resources)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
go build ./cmd/rogue
|
||||
# Configure and build
|
||||
./configure
|
||||
make
|
||||
|
||||
# Run the game
|
||||
./rogue
|
||||
```
|
||||
|
||||
**Note**: The executable name defaults to `rogue`, but may be different if configured with `--with-program-name`. Check the output of `make` to see the actual executable name.
|
||||
|
||||
**Note**: If you encounter compilation errors, especially related to ncurses compatibility, see [BUILD_ISSUES.md](BUILD_ISSUES.md) for known issues and workarounds.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required
|
||||
|
||||
- **C Compiler**: GCC or Clang (C89/C90 compatible)
|
||||
- **make**: Build automation tool (usually pre-installed)
|
||||
- **Linux**: Usually pre-installed, or `sudo apt-get install build-essential` (Debian/Ubuntu)
|
||||
- **macOS**: Included with Xcode Command Line Tools (`xcode-select --install`)
|
||||
- **FreeBSD**: `pkg install gmake` (or use `gmake` instead of `make`)
|
||||
- **ncurses library**: For terminal-based graphics
|
||||
- **Linux**: `sudo apt-get install libncurses5-dev` (Debian/Ubuntu)
|
||||
- **Linux**: `sudo yum install ncurses-devel` (RHEL/CentOS/Fedora)
|
||||
- **macOS**: `brew install ncurses` (Homebrew)
|
||||
- **FreeBSD**: `pkg install ncurses`
|
||||
|
||||
### Optional (for building from source)
|
||||
|
||||
- **Autotools**: autoconf, automake, m4 (needed if `configure` script doesn't exist)
|
||||
- **Linux**: `sudo apt-get install autoconf automake m4` (Debian/Ubuntu)
|
||||
- **Linux**: `sudo yum install autoconf automake m4` (RHEL/CentOS/Fedora)
|
||||
- **macOS**: Usually pre-installed with Xcode Command Line Tools, or `brew install autoconf automake`
|
||||
- **FreeBSD**: `pkg install autoconf automake m4`
|
||||
- **Note**: If the `configure` script already exists in the repository, you don't need these tools
|
||||
|
||||
### Optional (for building documentation)
|
||||
|
||||
- **Documentation tools**: groff/nroff, tbl, colcrt, sed (for generating man pages and docs)
|
||||
- **Linux**: Usually pre-installed, or `sudo apt-get install groff` (Debian/Ubuntu)
|
||||
- **macOS**: Usually pre-installed
|
||||
- **Note**: Documentation can be built later with `make` if these tools are available
|
||||
- **Note**: Generated documentation (man pages, HTML, etc.) is adapted to match the actual codebase outcomes and build configuration (e.g., executable name, scoreboard file location)
|
||||
|
||||
### Platform Support
|
||||
|
||||
This codebase supports multiple platforms:
|
||||
- **Unix-like systems** (Linux, macOS, FreeBSD, etc.)
|
||||
- **Windows** (via Visual Studio project files or MinGW/MSYS2)
|
||||
- **DOS** (DJGPP)
|
||||
- **Cygwin**
|
||||
|
||||
---
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Standard Build (Recommended)
|
||||
|
||||
The project uses Autotools for configuration. The build process depends on whether the `configure` script already exists:
|
||||
|
||||
**If `configure` script exists** (most common case):
|
||||
```bash
|
||||
# Restore a saved game
|
||||
# Configure the build system
|
||||
./configure
|
||||
|
||||
# Compile
|
||||
make
|
||||
|
||||
# Optional: Install system-wide (requires root)
|
||||
sudo make install
|
||||
```
|
||||
|
||||
**If `configure` script does NOT exist** (e.g., fresh git clone without generated files):
|
||||
```bash
|
||||
# Generate configure script (requires autoconf, automake, m4)
|
||||
autoreconf -fiv
|
||||
|
||||
# Configure the build system
|
||||
./configure
|
||||
|
||||
# Compile
|
||||
make
|
||||
|
||||
# Optional: Install system-wide (requires root)
|
||||
sudo make install
|
||||
```
|
||||
|
||||
**Note**: Most source distributions include the `configure` script, so you typically only need `autoreconf` if you're building directly from a git repository that doesn't include generated files.
|
||||
|
||||
**Note**: The executable name and other build outputs are determined by the `configure` script based on the codebase configuration. If you're using `Makefile.std` directly (manual build), the default executable name is `rogue54`, whereas `configure` defaults to `rogue`. Always check the actual output of your build process to confirm the executable name.
|
||||
|
||||
### Configure Options
|
||||
|
||||
The `configure` script supports several options:
|
||||
|
||||
```bash
|
||||
# Enable wizard mode (debug/cheat mode)
|
||||
./configure --enable-wizardmode
|
||||
|
||||
# Set custom scoreboard file location
|
||||
./configure --enable-scorefile=/path/to/scoreboard.scr
|
||||
|
||||
# Disable scoreboard
|
||||
./configure --enable-scorefile=no
|
||||
|
||||
# Set custom program name
|
||||
./configure --with-program-name=myrogue
|
||||
|
||||
# Install as setgid (for shared scoreboard)
|
||||
./configure --enable-setgid=games
|
||||
|
||||
# See all options
|
||||
./configure --help
|
||||
```
|
||||
|
||||
### Manual Build (Without Autotools)
|
||||
|
||||
If you prefer to build manually or the configure script fails:
|
||||
|
||||
**Option 1: Using Makefile.std**
|
||||
```bash
|
||||
# Build using the standard Makefile
|
||||
make -f Makefile.std
|
||||
|
||||
# Note: This creates 'rogue54' by default (see Makefile.std to change)
|
||||
```
|
||||
|
||||
**Option 2: Direct compilation**
|
||||
```bash
|
||||
# Compile directly (may need additional defines)
|
||||
gcc -O2 -o rogue *.c -lcurses
|
||||
|
||||
# Or with more defines (see Makefile.std for full list):
|
||||
gcc -O2 -DALLSCORES -DSCOREFILE=\"rogue.scr\" -DLOCKFILE=\"rogue.lck\" -o rogue *.c -lcurses
|
||||
```
|
||||
|
||||
**Note**:
|
||||
- Manual builds may require additional defines. See `Makefile.std` for reference.
|
||||
- The executable name may differ (`rogue` vs `rogue54` depending on build method).
|
||||
- You may need to adjust include paths if ncurses is in a non-standard location.
|
||||
|
||||
### Windows Build
|
||||
|
||||
#### Using Visual Studio
|
||||
|
||||
1. **Install PDCurses**:
|
||||
- Download PDCurses from https://pdcurses.org/
|
||||
- Extract to a directory (e.g., `C:\pdcurses`)
|
||||
- Build PDCurses library following its instructions
|
||||
|
||||
2. **Configure Visual Studio project**:
|
||||
- Open `rogue54.sln` in Visual Studio
|
||||
- Update include/library paths in project settings to point to your PDCurses installation
|
||||
- Ensure PDCurses library is linked (check project properties → Linker → Input)
|
||||
|
||||
3. **Build the solution**: Build → Build Solution (or press F7)
|
||||
|
||||
**Alternative**: If PDCurses is in a peer directory (`../pdcurses/`), the project may work without modification.
|
||||
|
||||
#### Using MinGW/MSYS2
|
||||
|
||||
```bash
|
||||
# Install ncurses via MSYS2
|
||||
pacman -S mingw-w64-x86_64-ncurses
|
||||
|
||||
# Configure and build
|
||||
./configure
|
||||
make
|
||||
```
|
||||
|
||||
**Note**: For MinGW/MSYS2, you may need to use `mingw32-make` instead of `make` depending on your installation.
|
||||
|
||||
---
|
||||
|
||||
## Running the Game
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Start a new game
|
||||
./rogue
|
||||
|
||||
# Restore from a saved game
|
||||
./rogue ~/rogue.save
|
||||
|
||||
# View high scores
|
||||
./rogue -s
|
||||
|
||||
# Test the death screen (demo mode)
|
||||
# Test death screen (demo mode)
|
||||
./rogue -d
|
||||
```
|
||||
|
||||
## In-game commands
|
||||
### In-Game Commands
|
||||
|
||||
Press `?` in game for the full list.
|
||||
- **`?`** - Show help/command list
|
||||
- **`/`** - Identify objects on screen
|
||||
- **Arrow keys** or **h/j/k/l** - Move
|
||||
- **`.`** - Wait/rest
|
||||
- **`i`** - Show inventory
|
||||
- **`e`** - Eat food
|
||||
- **`q`** - Quaff potion
|
||||
- **`r`** - Read scroll
|
||||
- **`w`** - Wield weapon
|
||||
- **`W`** - Wear armor
|
||||
- **`t`** - Throw object
|
||||
- **`z`** - Zap wand/staff
|
||||
- **`Q`** - Quit game
|
||||
|
||||
- **arrows** or **h/j/k/l/y/u/b/n** — move (shift to run, ctrl to run until
|
||||
adjacent)
|
||||
- **`.`** rest, **`s`** search for hidden doors and traps
|
||||
- **`i`** inventory, **`,`** pick up, **`d`** drop
|
||||
- **`q`** quaff potion, **`r`** read scroll, **`e`** eat food
|
||||
- **`w`** wield weapon, **`W`** wear armor, **`P`**/**`R`** put on / remove ring
|
||||
- **`t`** throw, **`z`** zap a wand, **`f`**/**`F`** fight
|
||||
- **`>`**/**`<`** take the stairs
|
||||
- **`S`** save, **`Q`** quit
|
||||
|
||||
## Environment
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Game options, as in the original
|
||||
export ROGUEOPTS="name=YourName,terse,jump,fruit=mango"
|
||||
# Set game options via environment
|
||||
export ROGUEOPTS="name=YourName,terse,jump"
|
||||
|
||||
# Wizard (debug) mode, with a reproducible dungeon
|
||||
ROGUE_WIZARD=1 SEED=12345 ./rogue
|
||||
# Wizard mode: set dungeon seed
|
||||
export SEED=12345
|
||||
./rogue "" # Empty string as first arg enables wizard mode
|
||||
```
|
||||
|
||||
The scoreboard is kept in `~/.rogue.scores`. Save files are Go gob snapshots
|
||||
and, as in the original, are deleted when restored.
|
||||
---
|
||||
|
||||
## Code layout
|
||||
## Project Structure
|
||||
|
||||
### Core Files
|
||||
|
||||
```
|
||||
game/ the game engine: one Go file per original C file,
|
||||
function-by-function (see ARCHITECTURE.md for the mapping)
|
||||
term/ tcell-backed terminal, replacing curses
|
||||
cmd/rogue/ the executable
|
||||
rogue.h # Main header with data structures and defines
|
||||
extern.h # External declarations and platform defines
|
||||
main.c # Entry point, initialization, main game loop
|
||||
command.c # Command processing and input handling
|
||||
```
|
||||
|
||||
The engine package is fully headless-testable: `make test` runs scripted command
|
||||
sequences, dungeon-generation golden checks, and an RNG compatibility test
|
||||
against the original C generator.
|
||||
### Game Systems
|
||||
|
||||
For development, the `Makefile` wraps the toolchain: `make fmt` (gofmt +
|
||||
prettier), `make lint` (golangci-lint), `make test` (the suite, under the race
|
||||
detector with coverage and a timeout), and `make check` (all three). Use the
|
||||
targets rather than invoking `go test` directly — they carry the flags the
|
||||
project relies on.
|
||||
**Note**: All source files are in the root directory. The structure below is logical grouping, not actual directory structure.
|
||||
|
||||
**Combat System**:
|
||||
```
|
||||
fight.c # Combat mechanics
|
||||
weapons.c # Weapon types and properties
|
||||
armor.c # Armor types and properties
|
||||
```
|
||||
|
||||
**Monster System**:
|
||||
```
|
||||
monsters.c # Monster definitions and stats
|
||||
chase.c # Monster AI and pathfinding
|
||||
```
|
||||
|
||||
**Item System**:
|
||||
```
|
||||
potions.c # Potion types and effects
|
||||
scrolls.c # Scroll types and effects
|
||||
rings.c # Ring types and effects
|
||||
sticks.c # Wand/staff types and effects
|
||||
things.c # General item handling
|
||||
```
|
||||
|
||||
**Dungeon Generation**:
|
||||
```
|
||||
rooms.c # Room generation
|
||||
passages.c # Corridor generation
|
||||
new_level.c # Level creation and initialization
|
||||
```
|
||||
|
||||
**User Interface**:
|
||||
```
|
||||
io.c # Input/output handling
|
||||
list.c # Inventory and object lists
|
||||
rip.c # Death screen
|
||||
```
|
||||
|
||||
### Supporting Systems
|
||||
|
||||
```
|
||||
daemon.c # Background processes (monster movement, hunger, etc.)
|
||||
daemons.c # Daemon management
|
||||
move.c # Player and monster movement
|
||||
pack.c # Inventory management
|
||||
save.c # Save/load game state
|
||||
state.c # Game state management
|
||||
init.c # Initialization routines
|
||||
extern.c # Global variable definitions
|
||||
```
|
||||
|
||||
### Platform Abstraction
|
||||
|
||||
```
|
||||
mach_dep.c # Machine-dependent code detection
|
||||
mdport.c # Platform abstraction layer
|
||||
```
|
||||
|
||||
### Build System
|
||||
|
||||
```
|
||||
configure.ac # Autoconf configuration
|
||||
Makefile.in # Makefile template
|
||||
config.h.in # Config header template
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Guide
|
||||
|
||||
### Code Style
|
||||
|
||||
This codebase follows classic C (pre-C99) conventions:
|
||||
|
||||
- Uses `register` keyword for frequently accessed variables
|
||||
- Custom macros for control flow (`when`, `otherwise`, `on()`, etc.)
|
||||
- Linked lists for dynamic data structures
|
||||
- Bit flags for object/monster states
|
||||
- Function declarations in headers, definitions in `.c` files
|
||||
|
||||
### Key Data Structures
|
||||
|
||||
#### `THING` (union)
|
||||
Represents both monsters and objects:
|
||||
```c
|
||||
union thing {
|
||||
struct { // Monster/player
|
||||
coord t_pos;
|
||||
struct stats t_stats;
|
||||
short t_flags;
|
||||
// ...
|
||||
} _t;
|
||||
struct { // Object
|
||||
int o_type;
|
||||
int o_which;
|
||||
int o_hplus, o_dplus;
|
||||
// ...
|
||||
} _o;
|
||||
};
|
||||
```
|
||||
|
||||
#### `PLACE`
|
||||
Represents a map cell:
|
||||
```c
|
||||
typedef struct {
|
||||
char p_ch; // Character to display
|
||||
char p_flags; // Flags (seen, passage, etc.)
|
||||
THING *p_monst; // Monster at this location
|
||||
} PLACE;
|
||||
```
|
||||
|
||||
### Game Loop Flow
|
||||
|
||||
```
|
||||
main()
|
||||
├─> Initialize (curses, player, objects)
|
||||
├─> new_level() # Generate first level
|
||||
├─> Start daemons # Background processes
|
||||
└─> playit()
|
||||
└─> while(playing)
|
||||
└─> command()
|
||||
├─> do_daemons(BEFORE)
|
||||
├─> Read input
|
||||
├─> Execute command
|
||||
├─> do_daemons(AFTER)
|
||||
└─> Monster movement
|
||||
```
|
||||
|
||||
### Daemons and Fuses
|
||||
|
||||
**Daemons** are background processes that run every turn:
|
||||
- `runners()` - Monster movement
|
||||
- `doctor()` - Health regeneration
|
||||
- `stomach()` - Hunger system
|
||||
- `swander()` - Wandering monsters
|
||||
|
||||
**Fuses** are one-time delayed actions:
|
||||
- Used for temporary effects (haste, confusion, etc.)
|
||||
|
||||
### Adding New Features
|
||||
|
||||
1. **New Monster Type**:
|
||||
- Add entry to `monsters[]` array in `monsters.c`
|
||||
- Update monster generation logic in `new_level.c`
|
||||
|
||||
2. **New Item Type**:
|
||||
- Add type constant to `rogue.h`
|
||||
- Add info structure (e.g., `pot_info[]` for potions)
|
||||
- Implement effect in corresponding file (e.g., `potions.c`)
|
||||
|
||||
3. **New Command**:
|
||||
- Add case in `command()` function in `command.c`
|
||||
- Implement handler function
|
||||
|
||||
### Debugging
|
||||
|
||||
#### Enable Wizard Mode
|
||||
|
||||
```bash
|
||||
./configure --enable-wizardmode
|
||||
make
|
||||
./rogue "" # Empty string enables wizard password prompt
|
||||
```
|
||||
|
||||
Wizard mode provides:
|
||||
- See all monsters (`SEEMONST` flag)
|
||||
- Set dungeon seed via `SEED` environment variable
|
||||
- Debug commands (see `wizard.c`)
|
||||
|
||||
#### Compile with Debug Symbols
|
||||
|
||||
```bash
|
||||
./configure CFLAGS="-g -O0"
|
||||
make
|
||||
gdb ./rogue
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Test death screen
|
||||
./rogue -d
|
||||
|
||||
# Test scoreboard
|
||||
./rogue -s
|
||||
|
||||
# Test save/restore
|
||||
./rogue
|
||||
# Play a bit, save (S command), quit
|
||||
./rogue ~/rogue.save # Should restore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Here's how to get started:
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. **Fork the repository** (if using git)
|
||||
2. **Create a branch** for your changes
|
||||
3. **Make your changes** following the code style
|
||||
4. **Test thoroughly** - play the game, test edge cases
|
||||
5. **Submit a pull request** or patch
|
||||
|
||||
### Contribution Guidelines
|
||||
|
||||
- **Code Style**: Follow existing conventions (see [Development Guide](#development-guide))
|
||||
- **Testing**: Test on multiple platforms if possible
|
||||
- **Documentation**: Update relevant comments/docs
|
||||
- **Commits**: Write clear commit messages
|
||||
- **Scope**: Keep changes focused and atomic
|
||||
|
||||
### Areas Needing Help
|
||||
|
||||
- **Bug fixes**: Check for known issues or test edge cases
|
||||
- **Platform support**: Improve Windows/DOS/Cygwin compatibility
|
||||
- **Code cleanup**: Modernize while maintaining compatibility
|
||||
- **Documentation**: Improve code comments and user docs
|
||||
- **Performance**: Optimize hot paths
|
||||
- **Accessibility**: Improve terminal compatibility
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
When reporting bugs, please include:
|
||||
- Platform and OS version
|
||||
- Compiler and version
|
||||
- Steps to reproduce
|
||||
- Expected vs. actual behavior
|
||||
- Any error messages
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Note**: For detailed information about known build issues, especially on modern systems, see [BUILD_ISSUES.md](BUILD_ISSUES.md).
|
||||
|
||||
### Build Issues
|
||||
|
||||
**Problem**: `configure: error: curses library not found`
|
||||
|
||||
**Solution**: Install ncurses development package:
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt-get install libncurses5-dev
|
||||
|
||||
# RHEL/CentOS/Fedora
|
||||
sudo yum install ncurses-devel
|
||||
|
||||
# macOS
|
||||
brew install ncurses
|
||||
```
|
||||
|
||||
**Problem**: `autoreconf: command not found` or `autoconf: command not found`
|
||||
|
||||
**Solution**: Install autotools (only needed if `configure` script doesn't exist):
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt-get install autoconf automake m4
|
||||
|
||||
# RHEL/CentOS/Fedora
|
||||
sudo yum install autoconf automake m4
|
||||
|
||||
# macOS (usually pre-installed with Xcode)
|
||||
xcode-select --install
|
||||
# Or via Homebrew:
|
||||
brew install autoconf automake
|
||||
|
||||
# FreeBSD
|
||||
pkg install autoconf automake m4
|
||||
```
|
||||
|
||||
**Note**: If the `configure` script already exists, you don't need these tools. Only run `autoreconf` if you're building from a git repository without generated files.
|
||||
|
||||
**Problem**: Compilation errors about undefined functions
|
||||
|
||||
**Solution**: Ensure `configure` was run successfully and `config.h` exists:
|
||||
```bash
|
||||
./configure
|
||||
make clean
|
||||
make
|
||||
```
|
||||
|
||||
**Problem**: Compilation errors about incomplete type 'WINDOW' or `curscr->_cury` / `curscr->_curx`
|
||||
|
||||
**Solution**: This is a known compatibility issue with modern ncurses. The codebase attempts to access internal ncurses structure members that are not available in modern ncurses libraries. See [BUILD_ISSUES.md](BUILD_ISSUES.md) for detailed information about this issue and potential fixes.
|
||||
|
||||
**Problem**: `make: command not found`
|
||||
|
||||
**Solution**: Install make:
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt-get install build-essential
|
||||
|
||||
# RHEL/CentOS/Fedora
|
||||
sudo yum groupinstall "Development Tools"
|
||||
|
||||
# macOS
|
||||
xcode-select --install
|
||||
|
||||
# FreeBSD (use gmake)
|
||||
pkg install gmake
|
||||
# Then use: gmake instead of make
|
||||
```
|
||||
|
||||
**Problem**: `configure: error: cannot find install-sh or install.sh`
|
||||
|
||||
**Solution**: The `install-sh` script should be in the repository. If missing, you may need to regenerate it:
|
||||
```bash
|
||||
autoreconf -fiv
|
||||
```
|
||||
|
||||
**Problem**: Documentation build fails (missing groff/nroff/tbl)
|
||||
|
||||
**Solution**: Documentation tools are optional. The game will build without them, but man pages won't be generated:
|
||||
```bash
|
||||
# Install documentation tools (optional)
|
||||
# Debian/Ubuntu
|
||||
sudo apt-get install groff
|
||||
|
||||
# RHEL/CentOS/Fedora
|
||||
sudo yum install groff
|
||||
|
||||
# Or skip documentation generation - the game will still build
|
||||
```
|
||||
|
||||
### Runtime Issues
|
||||
|
||||
**Problem**: Screen is too small
|
||||
|
||||
**Solution**: Rogue requires at least 24x80 terminal. Resize your terminal or use a larger font.
|
||||
|
||||
**Problem**: Terminal doesn't support colors/features
|
||||
|
||||
**Solution**: The game will auto-detect terminal capabilities. For best experience, use a modern terminal emulator (xterm, gnome-terminal, iTerm2, etc.).
|
||||
|
||||
**Problem**: Save file corruption
|
||||
|
||||
**Solution**: Save files are encrypted. Don't edit them manually. If corrupted, delete `~/rogue.save` and start fresh.
|
||||
|
||||
**Problem**: Scoreboard permission errors
|
||||
|
||||
**Solution**: If using setgid installation:
|
||||
```bash
|
||||
sudo chgrp games rogue.scr
|
||||
sudo chmod 664 rogue.scr
|
||||
```
|
||||
|
||||
### Platform-Specific
|
||||
|
||||
**Windows (MinGW)**: Ensure PDCurses is properly linked. Check `LIBS` in Makefile.
|
||||
|
||||
**macOS**: If using Homebrew ncurses, you may need to specify include/library paths:
|
||||
```bash
|
||||
# For Intel Macs (Homebrew in /usr/local)
|
||||
./configure CPPFLAGS="-I/usr/local/include" LDFLAGS="-L/usr/local/lib"
|
||||
|
||||
# For Apple Silicon Macs (Homebrew in /opt/homebrew)
|
||||
./configure CPPFLAGS="-I/opt/homebrew/include" LDFLAGS="-L/opt/homebrew/lib"
|
||||
|
||||
# Or let pkg-config find it (if available)
|
||||
./configure PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig"
|
||||
```
|
||||
|
||||
**Alternative**: If ncurses is installed but not found, you can also try:
|
||||
```bash
|
||||
# Check where ncurses is installed
|
||||
brew --prefix ncurses
|
||||
|
||||
# Use that path in configure
|
||||
./configure CPPFLAGS="-I$(brew --prefix ncurses)/include" LDFLAGS="-L$(brew --prefix ncurses)/lib"
|
||||
```
|
||||
|
||||
**Cygwin**: Ensure you're using the Cygwin version of ncurses, not a Windows port.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
### Documentation
|
||||
|
||||
- **In-game help**: Press `?` during gameplay
|
||||
- **Man page**: `man rogue` (after installation)
|
||||
- **Game guide**: See `rogue.doc` (generated from `rogue.me.in`)
|
||||
|
||||
### External Resources
|
||||
|
||||
- **Original Rogue**: The game that started it all
|
||||
- **Roguelike genre**: This game inspired NetHack, Angband, and many others
|
||||
- **Roguelike development**: Great learning resource for game programming
|
||||
|
||||
### Related Projects
|
||||
|
||||
- **NetHack**: Spiritual successor with more features
|
||||
- **Brogue**: Modern roguelike with beautiful ASCII graphics
|
||||
- **DCSS**: Dungeon Crawl Stone Soup, another modern roguelike
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
BSD-style; see [LICENSE.TXT](LICENSE.TXT).
|
||||
This project is licensed under a BSD-style license. See [LICENSE.TXT](LICENSE.TXT) for details.
|
||||
|
||||
**Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman**
|
||||
|
||||
Portions based on work by:
|
||||
- Nicholas J. Kisseberth (state.c, mdport.c)
|
||||
- David Burren (xcrypt.c)
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
This is the classic Rogue game that defined the roguelike genre. Thanks to the original authors for creating this timeless game, and to all contributors who have helped maintain and improve it over the years.
|
||||
|
||||
**Happy dungeon crawling!**
|
||||
|
||||
---
|
||||
|
||||
*For questions, issues, or contributions, please refer to the project's issue tracker or contact the maintainers.*
|
||||
|
||||
Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman.
|
||||
All rights reserved.
|
||||
|
||||
583
TODO.md
583
TODO.md
@@ -1,583 +0,0 @@
|
||||
# Workflow
|
||||
|
||||
- branch (from `main`)
|
||||
- do the work in Next Step
|
||||
- move Next Step to the top of Completed Steps
|
||||
- move the top item of Future Steps into Next Step
|
||||
- commit (`TODO.md` changes in the same commit as the work)
|
||||
- merge to `main` if the branch is not protected, otherwise open a PR
|
||||
- push
|
||||
|
||||
# Status
|
||||
|
||||
pre-1.0
|
||||
|
||||
The port on main is complete and faithful (function-by-function from Rogue 5.4.4
|
||||
C; reference sources on c-master/modern-rogue). Current phase: refactor from a
|
||||
transliterated port into idiomatic Go — one feature branch per step below,
|
||||
descriptive naming, real types, house style per
|
||||
~/dev/prompts/prompts/CODE_STYLEGUIDE_GO.md.
|
||||
|
||||
Refactor ground rules:
|
||||
|
||||
- Behavior must not change unless a step says so. The full test suite (scripted
|
||||
sessions, generation invariants, C-compatible RNG goldens) gates every step;
|
||||
80x24 seed-compatible gameplay stays intact.
|
||||
- Renames keep the C lineage greppable: doc comments retain their "(file.c
|
||||
func_name)" breadcrumbs, and the docs refresh step adds a C-name → Go-name
|
||||
table to ARCHITECTURE.md.
|
||||
|
||||
# Next Step
|
||||
|
||||
Broaden unit test coverage where playtesting finds thin spots (rings, sticks,
|
||||
wizard commands).
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-09 Wands and staffs under test (`test/sticks-coverage`, closes #6):
|
||||
the second of the three thin spots the Next Step names. `game/sticks.go` was
|
||||
the largest under-tested file in the repo — 534 lines, 23 functions, one test
|
||||
— and now has `game/sticks_test.go` (the zap handlers, `drain`, `fix_stick`,
|
||||
`charge_str`) and `game/bolt_test.go` (the `fire_bolt` geometry). Every
|
||||
expectation was read out of `sticks.c` rather than off the Go code; **no
|
||||
divergence from C was found**, and three things worth knowing came out of the
|
||||
reading. (1) **The bolt trail is the test instrument.** `fire_bolt` paints
|
||||
each square with `dirch` and then paints `chat()` back over every square it
|
||||
recorded, so on a screen nothing else has drawn on, the non-blank cells
|
||||
afterwards are exactly the squares the bolt occupied — and the walls it
|
||||
bounced off are absent, because C undoes the record with `c1--` and `break`s
|
||||
before the `mvaddch`. That gives an exact assertion of the path and the
|
||||
resting place without touching game code, and it is why the tests fire from a
|
||||
square that is not the hero's (which is what `chase.c` does for dragon
|
||||
breath): with the hero off the ray the run produces one message and the screen
|
||||
stays readable. (2) **A bounce reverses both components of the direction, not
|
||||
one.** A bolt entering a wall at 45 degrees goes back the way it came instead
|
||||
of reflecting off the surface, so the diagonal-into-a-vertical-wall case is
|
||||
the one that separates C's rule from the plausible wrong one, and it is
|
||||
tested. (3) **The `ch != 'M'` guard on the miss message is a tautology.** `ch`
|
||||
comes from `winat`, and `winat` _is_ `t_disguise` when a monster stands there
|
||||
(`rogue.h` 57), so `ch == 'M'` implies `t_disguise == 'M'` and the arm can
|
||||
never go quiet; it is vestigial from when 'M' was the mimic, and the test pins
|
||||
the port to speaking, so nobody "tidies" it into a real silence. The
|
||||
door-under-hero exception has no assertion of its own because it cannot have
|
||||
one: without it the bolt bounces on the hero's own square forever, recording
|
||||
nothing, and `fire_bolt` never returns — the test for it hangs rather than
|
||||
fails, which the comment on it says. Determinism comes from a `pinRng` helper
|
||||
that searches for a seed whose next draw is the wanted value (running the real
|
||||
`Rng`, never predicting it) and from a level the tests carve themselves
|
||||
through `drawRoom`, since bounce geometry and `drain`'s room/passage/door
|
||||
reach only mean something against known walls and a known passage number. All
|
||||
21 mutations tried against the new tests were caught.
|
||||
|
||||
- 2026-08-09 Command dispatch audit (`audit/command-switch-coverage`, closes
|
||||
#31): checked every case label in C's `command.c` against this port's
|
||||
dispatch, and left the audit behind as a standing test
|
||||
(`game/dispatch_test.go`) so the two cannot silently drift again. **No further
|
||||
missing keys were found** — `'+'` (#11) was the only one. That is the result,
|
||||
and it is worth recording as a negative: the class of bug exists, it has now
|
||||
been searched for exhaustively rather than stumbled upon, and the search came
|
||||
back empty. Three tables transcribe C's labels with their line numbers:
|
||||
main-switch keys answered from `commandHandlers`, main-switch keys whose arms
|
||||
need `dispatchKey`'s own switch (the `goto over` re-dispatches, `F`-to-`f`,
|
||||
`a`, `m`), and the `if (wizard)` sub-switch. `commandHandlers` is pinned by
|
||||
set equality in **both** directions: a missing key is the `'+'` bug, and an
|
||||
extra key is the same bug mirrored — a MASTER debug command leaking into
|
||||
ordinary play. Two traps make this audit harder than it sounds and are
|
||||
documented in the file: `rogue.h` 52-53 defines `when` as `break;case`, so a
|
||||
grep for `case ` finds ten of the eighty labels; and the main/wizard split is
|
||||
load-bearing, since `'+'` was a divergence in ordinary play precisely because
|
||||
it is a main-switch key. Confirms the port targets the MASTER build — all four
|
||||
`#ifdef MASTER` sites in `command.c` are ported unconditionally, as is
|
||||
`sticks.c` 237.
|
||||
|
||||
- 2026-08-09 Three small lost C behaviors (`fix/lost-c-behaviors`, closes #13):
|
||||
grouped because each is a few lines and all are "restore something the port
|
||||
dropped silently". (1) **"what a bizarre schtick!"**, `sticks.c` 237 — the
|
||||
`otherwise` arm that closes `do_zap`'s switch, which `doZap` had turned into
|
||||
doing nothing at all. Two things about it are easy to get wrong and are why
|
||||
the fix is not one line. It is under `#ifdef MASTER`, **not** under a `wizard`
|
||||
test, so in the MASTER build this port is it printed for every player — gating
|
||||
it on `g.Wizard` would be issue #11's trap in reverse. And `WS_NOP` is a case
|
||||
of that switch in its own right (`when WS_NOP: break;`), so "no handler ran"
|
||||
cannot be the trigger: the wand of nothing does nothing _quietly_, and only a
|
||||
kind C had no case for is bizarre. Since C's switch covers all 14 `WS_`
|
||||
values, its `otherwise` is reachable only for an `o_which` outside the table,
|
||||
which is exactly what `Object.hasValidWhich` already screens for — so the
|
||||
split needed no new state, just a three-way switch on handler / valid-Which /
|
||||
neither. All three arms fall through to `obj.Charges--`, as C's do: even the
|
||||
bizarre schtick costs a charge. Replaces the deferral comment PR #20 left
|
||||
there. (2) **`CTRL('R')` now actually redraws.** C is
|
||||
`after = FALSE; clearok(curscr, TRUE); wrefresh(curscr);` (`command.c`
|
||||
288-291); the port called `g.refresh()`, the ordinary diffing blit, **which
|
||||
cannot fix the only situation the command exists for** — a screen corrupted by
|
||||
something else's output leaves the game's record of it still correct, so the
|
||||
diff sends nothing and the corruption stays. New `Terminal.Repaint` (tcell
|
||||
`Screen.Sync`, which discards tcell's record of the terminal instead of
|
||||
diffing against it), `Screen.Repaint`, `g.repaint()`; three implementations to
|
||||
update, the same shape as PR #26's `ReadChar` change, so no split was needed.
|
||||
Named for the curses operation, not for tcell: the interface is the game's
|
||||
abstraction. It repaints what was last rendered — C repainted `curscr`, not
|
||||
`stdscr` — so it takes no window, and the arm drops the `refresh()` C never
|
||||
had there (`command` refreshes before the next key read anyway). (3) **The
|
||||
startup greeting**, `main.c` 107-113, which existed nowhere in the tree. New
|
||||
`game.Greeting`, printed by `cmd/rogue/main.go` before `term.New()` — the
|
||||
port's `initscr()`. Only the wizard wording is `#ifdef MASTER`; the other is
|
||||
unconditional. The `%d` is `dnum`, which `main.c` has just assigned to `seed`,
|
||||
so it is `Params.Seed`. Two placement details the issue did not mention and
|
||||
the tests now pin: the printf sits **after** `parse_opts`, so a ROGUEOPTS
|
||||
`name=` is what the player is greeted by and the account name is only the
|
||||
fallback (`Greeting` re-runs `ParseOpts`, which does nothing but assign into
|
||||
fields — no RNG, no screen); and it sits after the `-s`/`-d` handling and
|
||||
after `restore()`, which never returns, so a resumed game does not announce
|
||||
that a dungeon is being dug (`digsNewDungeon`). The game `Greeting` parses
|
||||
into is a throwaway but is built the way `New` builds the real one, tables and
|
||||
home directory included, because `ParseOpts` handles every option and not just
|
||||
the one the greeting reads: `inven=` is matched against `inv_t_name[]`, which
|
||||
lives on the game, so a bare `&RogueGame{}` turned a legal `ROGUEOPTS` into a
|
||||
nil dereference before the player saw a character. No RNG call is added on any
|
||||
path and nothing under `game/testdata/` moved; `TestSeedCompatItemTables` is
|
||||
green against the untouched golden. Mutation-proved, each new behaviour
|
||||
deleted in turn and only its own test failing: dropping the message arm fails
|
||||
`TestZapUnhandledWandSaysBizarreSchtick`; extending it to `WandNothing` fails
|
||||
`TestZapWandOfNothingIsSilent`; putting `g.refresh()` back fails
|
||||
`TestRedrawCommandForcesFullRepaint`; swapping the two wordings, or the
|
||||
ROGUEOPTS name for the account name, fails `TestGreeting`; greeting on the
|
||||
restore path fails `TestDigsNewDungeon`. ARCHITECTURE.md §5.3 gains `Repaint`
|
||||
and the paragraph on why a blit cannot substitute for it. `Next Step`
|
||||
deliberately not rotated: out-of-band issue work.
|
||||
|
||||
- 2026-08-09 The `'+'` wizard-mode toggle (`fix/wizard-toggle-off`, closes #11):
|
||||
C's `command.c` 317-338 has a `when '+'` arm that leaves wizard mode —
|
||||
`wizard = FALSE`, `turn_see(TRUE)`, `msg("not wizard any more")` — and the
|
||||
port had no `'+'` anywhere, so the key fell through `dispatchKey`'s default to
|
||||
`illcom` and answered "illegal command '+'". The password half of that arm was
|
||||
dropped on purpose (wizard mode is `ROGUE_WIZARD` configuration) and is in
|
||||
ARCHITECTURE.md §9; the leave half was lost silently and is not the same
|
||||
decision — it does not touch the password machinery at all. **The substantive
|
||||
part is `turn_see(TRUE)`**, not the flag: wizard sight draws every monster the
|
||||
hero cannot see, so without the re-hide there is no way back to normal
|
||||
visibility once wizard mode is on, and clearing the flag alone would have left
|
||||
the screen lying. New `wizardToggleCommand` in `game/command.go`, registered
|
||||
in `commandHandlers` between `'^'` and `Escape` — C's own switch order, and
|
||||
note that C's arm sits in the **main** command switch under `#ifdef MASTER`,
|
||||
not in the `if (wizard) switch (ch)` sub-switch that `wizardCommand` ports, so
|
||||
it is reachable whether or not `wizard` is set. That makes the non-wizard case
|
||||
a divergence too, and it resolves the way the dropped `passwd()` forces: a
|
||||
password check that no longer exists can never succeed, so the else arm is
|
||||
what C did on a wrong answer, the message "sorry" — no prompt, since nothing
|
||||
typed into one could change the outcome, and none of the
|
||||
`noscore`/`turn_see(FALSE)` bookkeeping of C's unreachable success branch. The
|
||||
choice is stated in the function's doc comment and in §9, whose password row
|
||||
now names the `'+'` enter arm and whose new paragraph records that the leave
|
||||
arm is ported in full. Two tests in `game/wizard_test.go` drive `'+'` through
|
||||
`g.dispatch`: the wizard one spawns a phantom (`ISINVIS` straight from the
|
||||
monster table, so `seeMonst` is false and it is on screen only because wizard
|
||||
sight put it there), asserts the precondition — monster glyph drawn in
|
||||
standout at its cell, `SenseMonsters` set — and then asserts the flag cleared,
|
||||
`SenseMonsters` cleared, the cell back to the map char under the monster with
|
||||
standout off, the exact message, and `After` false; the non-wizard one pins
|
||||
"sorry" and that `'+'` is no longer an illegal command. Mutation-proved:
|
||||
deleting the `turnSee(true)` call fails the test on all three visibility
|
||||
assertions, which is the half a flag-only test would have missed. No RNG call
|
||||
is added — the `turn_off` arm of `turn_see` never reaches `rnd`, only the
|
||||
turn-on arm does — and `TestSeedCompatItemTables` stays green against the
|
||||
untouched golden. `Next Step` deliberately not rotated: out-of-band issue
|
||||
work.
|
||||
|
||||
- 2026-08-09 Cleanups deferred from the PR #26 review (`cleanup/pr26-followups`,
|
||||
closes #27): four items, no behaviour change. (1) The `sig-leave` entry below
|
||||
still argued, in the present tense, that declining to save on SIGINT/SIGQUIT
|
||||
was the safe choice because `AutoSave` encodes live state after removing the
|
||||
file — both halves untrue since #24, and the entry read as a claim about how
|
||||
the code works now rather than a record of what was weighed then. It is in the
|
||||
past tense and marked superseded, pointing at the `fix/autosave-race` entry.
|
||||
Nothing else in the file was touched — in particular the `err113` linter name
|
||||
in the 2026-07-06 entry, which a `grep` for `113` still matches, and the "over
|
||||
a hundred reports" wording the #26 rework had already corrected. Note for
|
||||
anyone chasing this class of bug: the false claim was in the #12 entry, not
|
||||
the #24 one, whose account of the old remove-then-write is correctly past
|
||||
tense — find these by content, since `make fmt` reflows the file and cited
|
||||
line numbers rot. (2) `encodeSnapshot` is `writeSnapshotFile`: it encodes,
|
||||
fsyncs, chmods 0400 and closes, and the old name claimed only the first of
|
||||
those. One call site (`saveFile`), and the doc comment now lists what it does
|
||||
and why the fsync is there. (3) `TestAutoSaveOnSignalWhileInShellEscape` used
|
||||
`t.Error` for its precondition, so a save that was never taken fell through
|
||||
into `assertRestorable`, which can then only report a second, derived failure;
|
||||
it is `t.Fatal`, matching the identical assertion in the blocked-on-input
|
||||
test. (4) `serviceAutoSaveRequest`'s doc comment had a 24-column stub line
|
||||
("The result is still a") left by an earlier edit — `gofmt` does not rewrap
|
||||
comments, so `fmt-check` was legitimately green and nothing would ever have
|
||||
caught it. Rewrapped to the block's width. `Next Step` deliberately not
|
||||
rotated: out-of-band issue work.
|
||||
|
||||
- 2026-08-09 Signal-time autosave moved onto the game goroutine
|
||||
(`fix/autosave-race`, closes #24): the SIGHUP/SIGTERM handler gob-encoded the
|
||||
live game tree from the signal goroutine while the game goroutine was mid-turn
|
||||
mutating it, and `AutoSave` **removed** the save file before encoding — so the
|
||||
failure mode was not a stale save but a deleted one followed by a possibly
|
||||
torn replacement, with a window in which the player had neither. `make test`
|
||||
has run with `-race` since 2026-08-09 and was green, because no test had ever
|
||||
driven the turn loop concurrently with a signal: evidence of untested, not of
|
||||
safe. The handler now writes nothing itself. `AutoSaveOnSignal` posts a
|
||||
request on a one-deep channel, wakes the input read, and waits up to
|
||||
`signalSaveTimeout` (3s) for the game goroutine to take it; the encode happens
|
||||
on the goroutine that owns the state. **The blocked-on-input case is the whole
|
||||
point** — a dropped connection lands while the player is thinking, so a flag
|
||||
checked only between turns would never be looked at — and it is handled by
|
||||
making the read interruptible: `Terminal.ReadChar` returns `(byte, bool)` with
|
||||
`ok == false` meaning "woken by `Interrupt`, no key", `term.Tcell.Interrupt`
|
||||
posts a `tcell.EventInterrupt` onto tcell's own event queue to unpark
|
||||
`PollEvent`, and `readchar` services the request and reads again, so no caller
|
||||
sees the wake-up. The other unbounded park is the `!` shell escape, where a
|
||||
hangup used to save and would otherwise have regressed to not saving: the
|
||||
shell now runs on a helper goroutine and `runShellEscape` selects on {shell
|
||||
finished, save request}, keeping the encode on the game goroutine while it
|
||||
draws nothing. Between turns (`command`) covers a game that is busy rather
|
||||
than parked. The wait is bounded so that a game goroutine wedged with no
|
||||
service point can never stop a signal from getting the process out; giving up
|
||||
costs nothing now that `saveFile` writes a temporary file in the save's own
|
||||
directory, fsyncs it, and renames it over the target instead of truncating in
|
||||
place — a failed or skipped save leaves the previous save whole. New
|
||||
`game/autosave_test.go` drives the real turn loop while a second goroutine
|
||||
asks for 25 saves (the interleaving that never existed before), plus the
|
||||
parked-on-input case with a terminal fake that genuinely blocks, the shell
|
||||
case, the deadline case (previous save byte-for-byte intact), the no-file-name
|
||||
case, and the rename discipline — the last pinned by a handle opened before
|
||||
the save, which still reads the old file whole after it. Each was
|
||||
mutation-proved: reverting `AutoSaveOnSignal` to encode on the calling
|
||||
goroutine (the pre-fix behavior) makes the turn-loop test fail under `-race`
|
||||
with over a hundred reports, and removing each of the three service points
|
||||
fails exactly the test for that park with its own message. `pendingSaver` now
|
||||
reads the game out from under its mutex instead of delegating with it held,
|
||||
because the delegated call blocks until the save is taken — the PR #23
|
||||
review's N3 note, load-bearing rather than hypothetical, and pinned by a test.
|
||||
The SIGINT/SIGQUIT no-save decision and the single-signal-read ordering
|
||||
guarantee are untouched; `savesOnSignal`'s third ground ("safety") is
|
||||
rewritten, since the corruption window it weighed no longer exists.
|
||||
`MEMORY.md` stops listing signal-time autosave among the deliberate `_ =`
|
||||
discards and states the new discipline; `ARCHITECTURE.md` §5.3, the `Terminal`
|
||||
sketch, the C-to-Go mapping row and §9's SIGTSTP paragraph are corrected to
|
||||
match. Two things review caught and this entry records so they are not undone:
|
||||
moving the shell onto a helper goroutine also moved `term.Tcell.ShellEscape`'s
|
||||
`panic` on a failed `Screen.Resume` there, and a panic at the top of any
|
||||
goroutine kills the process without running the deferred calls of the others —
|
||||
including `cmd/rogue/main.go`'s `defer t.Fini()`, so the tty would have been
|
||||
left raw on exactly the path where the terminal is already broken (issue #12's
|
||||
failure, reintroduced on a new path). `runShellEscape` recovers the helper's
|
||||
panic and re-raises it on the game goroutine, pinned by
|
||||
`TestShellEscapePanicUnwindsTheGameGoroutine`. And the doc comment took two
|
||||
rounds to get right: the first version claimed in four places that nothing is
|
||||
half-mutated at the `readchar` service point, and the revision that fixed that
|
||||
claimed two of the three service points were between-commands. Both are false.
|
||||
Only the check at the top of `command` is between commands — `readchar` is
|
||||
reached from mid-command prompts, and `runShellEscape` is reached from
|
||||
`shell`, an ordinary `'!'` command handler dispatched inside `command`, with
|
||||
that turn's `DoDaemons(Before)`/`DoFuses(Before)` already fired and its AFTER
|
||||
pass and ring effects not yet. What is actually guaranteed is that the encode
|
||||
runs on the state-owning goroutine, so the snapshot is internally consistent
|
||||
and restorable, though it may freeze a command half applied. `Next Step`
|
||||
deliberately not rotated: out-of-band issue work.
|
||||
|
||||
- 2026-08-09 Signal-time terminal restore (`sig-leave`, closes #12): the port
|
||||
handled only SIGHUP and SIGTERM, so SIGINT and SIGQUIT killed the process with
|
||||
tcell still holding the tty, leaving the user at a shell with no echo. All
|
||||
four signals now go to one `os/signal` channel read by one goroutine in
|
||||
`cmd/rogue/main.go`, and every path calls `Terminal.Fini` before `os.Exit(0)`
|
||||
— C's `leave()`, "leave quickly but curteously". **The decision** (written
|
||||
into the `savesOnSignal` comment): SIGHUP/SIGTERM keep autosaving,
|
||||
SIGINT/SIGQUIT restore and exit **without** saving. C never saves on INT or
|
||||
QUIT anywhere — `leave()` is endwin-and-exit, `quit()` confirms/scores/exits,
|
||||
`endit()` goes through `fatal()`, and `save.c auto_save` is reserved for
|
||||
HUP/TERM — and the semantics agree: HUP/TERM are involuntary teardown worth
|
||||
rescuing a game from, while INT/QUIT are a deliberate "stop now" that must not
|
||||
become a one-keystroke checkpoint against a save discipline built to be
|
||||
anti-save-scum. A third ground was weighed at the time and has since been
|
||||
superseded: back then `AutoSave` gob-encoded live state that the main
|
||||
goroutine was still mutating, after removing the old file, so declining to
|
||||
save on the signals with nothing to rescue was also the option with no
|
||||
corruption window. That window is gone as of the `fix/autosave-race` entry
|
||||
above (#24) — the encode now runs on the game goroutine and `saveFile` renames
|
||||
a temporary file into place — so nothing here should be read as a statement
|
||||
about how saving works now; the split stands on C and on semantics alone, as
|
||||
the current `savesOnSignal` comment says. The single-reader design closes the
|
||||
window the issue warned about: a second signal arriving mid-save stays unread
|
||||
in the buffer instead of exiting out from under the writer
|
||||
(`TestLeaveOnSignalIgnoresLaterSignals` reproduces exactly that interleaving).
|
||||
New `cmd/rogue/main_test.go` pins the membership of `handledSignals()` itself
|
||||
(`TestHandledSignalsSet` — without it the rest of the file, which iterates
|
||||
that set, would pass against a set that had silently lost SIGINT and SIGQUIT
|
||||
again), and covers the ordering for each signal, the save/no-save split
|
||||
against `savesOnSignal`, the mid-save-second-signal case, the pre-game
|
||||
`pendingSaver` window, and real SIGINT/SIGQUIT/SIGHUP/SIGTERM delivered to the
|
||||
test process through the same `notifySignals` wiring the game uses; the tty
|
||||
leaving raw mode is the one step not checkable headlessly (it needs a
|
||||
controlling terminal), and `term.Tcell.Fini` is a direct pass-through to
|
||||
tcell's `Screen.Fini` that `myExit` already depends on. Two premises in the
|
||||
issue turned out to be wrong and are recorded in ARCHITECTURE.md: `leave()` is
|
||||
not installed on SIGINT/SIGQUIT during play (the wiring is in `mdport.c`, the
|
||||
shipped build calls `md_onsignal_default()` and installs nothing, and
|
||||
`leave()` appears only in the endgame paths of `rip.c`/`main.c`), and Ctrl-C
|
||||
never generated SIGINT here anyway, since tcell's raw mode clears `ISIG` and
|
||||
the key arrives as byte `0x03` — as it did in C, whose `setup()` calls curses
|
||||
`raw()`. The real exposure is `kill -INT`/`kill -QUIT`, a SIGINT to the
|
||||
process group while the `!` shell escape has the screen suspended, and the
|
||||
window **after** `term.New()`: nothing is raw before it, and the handlers used
|
||||
to be installed only once the game existed, leaving the restore path and
|
||||
`-d`'s `DeathDemo()` — which never returns, blocking in `waitFor` inside
|
||||
`death()` — running raw with no handler at all. The handlers are therefore
|
||||
installed immediately after `term.New()`, with the game handed to them
|
||||
afterwards via `pendingSaver`; a signal before the game exists restores the
|
||||
terminal and exits with nothing to save, and the SIGHUP/SIGTERM autosave
|
||||
behavior on the play path is unchanged. ARCHITECTURE.md §9 gained rows for
|
||||
SIGTSTP/`tstp()` (dropped: raw mode means Ctrl-Z cannot reach us, a suspend
|
||||
from the signal goroutine would race the drawing goroutine, and C armed `tstp`
|
||||
only after a `restore()`; the `!` shell escape covers the need), for SIGINT
|
||||
not routing to the interactive `quit()` prompt, and for `auto_save` on the
|
||||
fault signals; §5.3's claim that tcell handles SIGTSTP was false — tcell
|
||||
registers only SIGWINCH — and is corrected. `Next Step` deliberately not
|
||||
rotated: out-of-band issue work.
|
||||
|
||||
- 2026-08-09 Wizard-create bounds fix (`fix/wizard-which-bounds`, closes #10):
|
||||
`createObj` stored the raw `0-f` nibble as `Object.Which` with no bounds
|
||||
check, so wizard mode -> `C` -> `/` -> `f` produced a wand numbered 15 against
|
||||
a 14-entry table and panicked in `fixStick`. Input outside `0-f` overshoots
|
||||
much further rather than going negative: `readchar` returns a `byte`, so the
|
||||
`int(ch-'a') + 10` branch is byte arithmetic and wraps — `'A'` gives 234 and
|
||||
`'!'` gives 202 — and panicked the same way. C's `create_obj()` was equally
|
||||
unchecked, but every C consumer was either a `switch` (defined for any value)
|
||||
or a static-array read past the end (undefined, and survivable in practice),
|
||||
whereas since refactor step 8 one game is one process, so the Go panic kills
|
||||
the game with the terminal still in raw mode. Fixed at the two boundaries a
|
||||
bad `Which` can enter through: `createObj` now rejects an out-of-range choice
|
||||
with a message built from C's own `type_name()` vocabulary and adds nothing to
|
||||
the pack (a deliberate, commented divergence, since C had no defined behavior
|
||||
here to be faithful to), and `Restore` refuses a snapshot describing such an
|
||||
object (`ErrSaveCorrupt`) instead of loading a game that would explode later.
|
||||
Behind those, `whichLimit`/`hasValidWhich` back defensive guards at every
|
||||
dispatch named in the issue: the three effect tables (the new `quaffHandler`,
|
||||
`readHandler`, and `zapHandler` accessors return no handler rather than
|
||||
indexing — for wands that is exactly non-`MASTER` C, which matched no case and
|
||||
still ran `o_charges--`), the `callIt` lore lookups, `identifyType` (whose
|
||||
table is shorter than the scroll table keying it, though no scroll that can
|
||||
reach `readIdentify` overshoots it, so that one is defensive rather than a
|
||||
live bound), `armorClass` for the four `a_class[]` reads, `initWeapon` against
|
||||
the missing `init_dam[]` row for `WeaponFlame`, `fixStick`'s `ws_type[]` read,
|
||||
and `inventoryName`, hoisted so one check covers the scroll-title read the
|
||||
issue listed plus its potion-color, ring-stone, wand-material, weapon and
|
||||
armor siblings. `objectWorth` got the same hoisted guard, since the
|
||||
death-screen appraisal reads the identical per-kind tables. No in-range input
|
||||
changes behavior and no guard consumes a random number — the rejection
|
||||
precedes every `rnd()` call, verified both by an explicit seed-unchanged test
|
||||
and by `TestSeedCompatItemTables` staying green untouched. New
|
||||
`game/wizard_test.go`: the exact reproducer, a rejection sweep over every
|
||||
indexed kind including both wrapping-input forms, an acceptance sweep proving
|
||||
valid choices still build the right item, one no-panic test per guarded family
|
||||
(wand/potion/scroll/armor/weapon), the `fixStick` crash site, the corrupt-save
|
||||
rejection over the wrapped values and a negative `Which` (a decoded snapshot
|
||||
is the only source of one, so it is what exercises the `Which >= 0` arm of
|
||||
`hasValidWhich`), and a check that `whichLimit` still agrees with the table
|
||||
sizes. Each guard was confirmed load-bearing by reverting it and watching the
|
||||
test panic. `Next Step` deliberately not rotated: this was out-of-band issue
|
||||
work.
|
||||
|
||||
- 2026-08-09 Stale-docs correction (`docs-staleness`, closes #3): four claims in
|
||||
`MEMORY.md`/`TODO.md`/`README.md` had gone false and were misdirecting agents
|
||||
— the reviewer on PR #9 repeated one of them verbatim. Each was re-verified
|
||||
against the tree before rewriting. (1) `MEMORY.md` described C's `exit()`
|
||||
being unwound by a `gameEnd` panic recovered in `Run`; refactor step 8 deleted
|
||||
that, `gameEnd` appears nowhere in the sources, and `myExit` (`game/rip.go`)
|
||||
now calls `Terminal.Fini` then `os.Exit(0)` while `Run()` never returns — so
|
||||
the section states the exit model and its testing consequence (a death exits
|
||||
the test binary; hence `fortify()` in `game/run_test.go`). (2) `MEMORY.md`
|
||||
said approved lint exceptions live in a "Repo-specific exceptions" block in
|
||||
`.golangci.yml`; no such block exists and the config is byte-identical to
|
||||
canonical (sha256 `021cc83f…46bcb`), the approvals having moved to in-code
|
||||
`//nolint` directives carrying their dates — and `paralleltest` was listed as
|
||||
an approved disable when it was in fact fixed (no `paralleltest` token in the
|
||||
tree; 32 `t.Parallel()` calls against 32 tests). (3) `MEMORY.md` "Debugging"
|
||||
and (4) `README.md` both told the reader to run `go test` directly, which
|
||||
since PR #9 silently drops `-timeout 30s -race -cover`; both now point at
|
||||
`make test`/`make check`. Also dropped the false "currently v2.12.2" host
|
||||
linter claim from the 2026-08-07 entry (the host is v2.10.1 and nothing is
|
||||
pinned; the pin question is tracked separately). Documentation only — no code,
|
||||
`Makefile`, or config change; `Next Step` deliberately not rotated, since this
|
||||
was out-of-band issue work.
|
||||
|
||||
- 2026-08-09 Policy-shaped `make test` (`make-test-policy-pattern`): the `test:`
|
||||
target was a bare `go test $(GO_PKGS)` and now runs
|
||||
`-timeout 30s -race -cover` with the mandated conditional verbose rerun (on
|
||||
failure it reruns with `-v` and then `exit 1`, so a flaky pass on the second
|
||||
attempt cannot rescue the build). `$(GO_PKGS)` is kept rather than hardcoding
|
||||
`./...`. The substance was `-race`, not the Makefile edit: this is the first
|
||||
time the suite has run under the race detector, and it is clean — no data
|
||||
races across five consecutive uncached runs, including the tcell terminal
|
||||
layer and the `os.Exit`-path playthrough tests. Wall clock 5.1s cold
|
||||
(including the race build) and ~2.3s warm, against the 20s policy budget. The
|
||||
failure path was exercised with a throwaway failing test to confirm the rerun
|
||||
fires and `make` exits non-zero. Build tooling only; no game behavior change.
|
||||
|
||||
- 2026-08-07 Canonical linter config (`golangci-v2.12.2`): replaced
|
||||
`.golangci.yml` with the shared canonical config (v2 schema; settings now live
|
||||
under `linters.settings`, so the `lll`/`funlen`/`cyclop`/`dupl` thresholds
|
||||
actually apply — the old top-level `linters-settings` block was silently
|
||||
ignored). The four repo-specific disables (`mnd`, `exhaustive`,
|
||||
`paralleltest`, `testpackage`) moved out of the config into targeted in-code
|
||||
`//nolint` directives carrying the original approval dates, so the config
|
||||
stays byte-identical to canonical. Real fixes: `t.Parallel()` in all 32 tests,
|
||||
24 long lines wrapped or their comments tightened, control bytes in
|
||||
`term/tcell.go` as character literals, and two `wsl_v5` defer cuddles. The
|
||||
repo has no golangci-lint version pin to bump (no Dockerfile or CI;
|
||||
`make lint` runs whatever `golangci-lint` is on the host).
|
||||
|
||||
- 2026-07-24 Seed compatibility — item tables (seed-compat): instrumented the C
|
||||
reference on modern-rogue with a DUMP mode (testdata/c_seedcompat.patch) that
|
||||
forces the RNG seed and prints the per-seed item appearance tables (potion
|
||||
colors, scroll names, ring stones, wand/staff materials) before initscr, and
|
||||
captured its output for four seeds as testdata/item_tables.golden.
|
||||
TestSeedCompatItemTables regenerates the same tables from the Go port and they
|
||||
match byte for byte — proving the LCG and its consumption order through the
|
||||
whole init sequence agree with C. The remaining "same dungeon (map)" half
|
||||
would need the harder headless-curses C dump (new_level draws to curses);
|
||||
deferred — the item-table match already validates RNG-order faithfulness
|
||||
through init, and the Go generation goldens guard determinism thereafter.
|
||||
|
||||
- 2026-07-23 Playtest hardening (playtest-hardening): added two death-safe
|
||||
crash-sweep drives through the real turn loop, within the step-8 os.Exit
|
||||
constraint (a fortify() helper pins HP/food/exp and clears the freeze/stuck
|
||||
counters each turn so no death exits the test binary; fixed seeds keep them
|
||||
deterministic). TestDeepPlaythrough uses quaff/read/zap through command
|
||||
dispatch, then descends to depth 8 with a save/restore at depth 4;
|
||||
TestTurnLoopCrashSweep mashes movement/search/rest for 200 turns on four
|
||||
seeds. Neither surfaced a panic. The interactive "play several games at a real
|
||||
tcell terminal" portion needs a human at an 80x24 terminal and is left to the
|
||||
maintainer; the binary's non-interactive paths (`-s` scores) were
|
||||
smoke-tested.
|
||||
|
||||
- 2026-07-23 Docs refresh (docs-refresh): rewrote ARCHITECTURE.md Part 2 (the
|
||||
pre-implementation design sketch) to match the final code — current type/field
|
||||
names (ObjectKind, DiceSpec, split o_arm, step-1 flag names, TrapCount, Level
|
||||
list methods), the static tables now on the per-game gameData struct, the
|
||||
daemon/effect handler tables, the MessageLine extraction, the Terminal
|
||||
interface, the flat gob SaveState, and the New(Params) + os.Exit design. Added
|
||||
§7.1, a C-name → Go-name rename table, and a README note on the make targets.
|
||||
|
||||
- 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
|
||||
tables plus a full decomposition sweep — the quaff / readScroll / doZap
|
||||
switches, the attack monster-power switch, the be_trapped switch, the daemon
|
||||
d_func switch, and the command-key switch all became handler tables on
|
||||
gameData (quaffHandlers, readHandlers, zapHandlers, hitHandlers, trapHandlers,
|
||||
daemonHandlers, commandHandlers), one small named method per case. Every
|
||||
remaining cyclop/gocognit/nestif hot spot was split into named helpers across
|
||||
fight, misc (look), command, chase, move, passages, options, pack, things,
|
||||
save, daemons, rooms, score, monsters, rings, rip, io, object, weapons,
|
||||
wizard, and term/tcell, plus three test functions. Effect order and RNG call
|
||||
sequence preserved throughout; the whole golangci-lint run is now 0 issues.
|
||||
|
||||
- 2026-07-07 Refactor step 6 (refactor/god-object-extraction): MessageLine (was
|
||||
MsgLine) owns the msg/addmsg/endmsg machinery, wired to its screen/look/input
|
||||
needs via attach(); RogueGame keeps one-line msg/addmsgf/endmsg shorthands so
|
||||
call sites are unchanged. Player owns pack bookkeeping (nextPackChar,
|
||||
removeFromPack — the state half of leave_pack; leavePack keeps only LastPick
|
||||
tracking). Level owns object/monster list management and lookup (ObjectAt
|
||||
replaces findObj; AddObject/RemoveObject/AddMonster/RemoveMonster replace
|
||||
direct attachObj/detachObj/attachMon/detachMon on level lists).
|
||||
Inventory/pickup UI flows stay on RogueGame deliberately: they are display and
|
||||
turn orchestration, not state surgery.
|
||||
|
||||
- 2026-07-07 Refactor step 5 (refactor/item-combat-ui-renames, three commits,
|
||||
one subsystem each): items — getItem→promptPackItem now returning (obj, ok),
|
||||
invName→inventoryName, doPot→applyPotionFuse; combat — rollEm→rollAttacks,
|
||||
attack/moveMonster/chaseStep return (removed bool) instead of C -1/0 int
|
||||
codes; UI — getDir→promptDirection. C breadcrumbs kept; suite green.
|
||||
|
||||
- 2026-07-07 Refactor step 4 (refactor/movement-renames): movement/world renames
|
||||
(doMove→moveHero, beTrapped→springTrap, rndmove→randomStep,
|
||||
doRooms/doPassages/doMaze→digRooms/digPassages/digMaze, chgStr→changeStrength,
|
||||
doRun→startRun, moveStuff→finishMove, turnref→turnRefresh,
|
||||
moveMonst→moveMonster, doChase→chaseStep, setOldch→setOldChar, cansee→canSee,
|
||||
roomin→roomIn, runto→runTo, conn→connectRooms, putpass→putPassage,
|
||||
passnum→numberPassages, numpass→numberPassage, rndPos→randomPos,
|
||||
rndRoom→randomRoom, treasRoom→treasureRoom, accntMaze→accountMaze); all
|
||||
goto/label flows replaced with loops (moveHero retry loop + extracted
|
||||
passageTurn, dispatch re-dispatch loop, chaseStep passage loop, saveGame
|
||||
labeled prompt loop); C breadcrumbs kept in doc comments.
|
||||
- 2026-07-07 Lint adoption finished (refactor/no-package-globals): all 37
|
||||
package-level vars moved into `gameData` (built by `newGameData`, hung on
|
||||
RogueGame as `g.data`, set in NewGame and Restore); ObjectKind
|
||||
Glyph()/objectKindForGlyph became switches; the table-reading subtype
|
||||
Stringers were removed; isMagic became a RogueGame method; goconst fixed with
|
||||
named word constants (potionName, goldName, staffName, ripWall, ...);
|
||||
testpackage and exhaustive disabled in .golangci.yml with sneak's approval
|
||||
(2026-07-07); misspell's corruption of the "ther" scroll syllable reverted.
|
||||
mnd disabled with sneak's approval (2026-07-07, follow-up commit). Remaining
|
||||
red: cyclop (36), nestif (30), gocognit (23) stay until step 7 fixes them per
|
||||
sneak's ruling.
|
||||
- 2026-07-06 Lint adoption bulk (refactor/lint-adoption, 5ba9fe8): .golangci.yml
|
||||
copied verbatim from the prompts repo (plus the sneak-approved paralleltest
|
||||
exception, 2026-07-06); ~1,500 findings fixed (autofix formatting sweep,
|
||||
errcheck/err113/noinlineerr error handling, forbidigo, funcorder, recvcheck
|
||||
pointer receivers, goprintffuncname renames msg helpers to *f, revive doc
|
||||
comments, gocritic switch rewrites, gosec real fixes plus justified nolints,
|
||||
unparam signature tightening, C-faithful "missle" spellings restored after
|
||||
misspell autofix changed game text).
|
||||
- 2026-07-06 Module base path updated to git.eeqj.de/sneak/rgoue (go.mod, term/
|
||||
and cmd/ imports, ARCHITECTURE.md, version string).
|
||||
- 2026-07-06 Refactor step 3 (refactor/object-fields): Object.Arm split into
|
||||
ArmorClass/Charges/GoldValue/Bonus (rings); Stats.Arm → ArmorClass; damage
|
||||
strings parsed once into DiceSpec at table definition (ParseDice keeps C
|
||||
roll_em parse semantics, incl. "%%%x0" and "000x0" edge cases,
|
||||
regression-tested); save format 5.4.4-go3.
|
||||
- 2026-07-06 Refactor step 2 (refactor/typed-kinds, b940cfc): ObjectKind
|
||||
separates item category from map glyph (Object.Type byte → Kind ObjectKind
|
||||
with Glyph()); PotionKind/ScrollKind/RingKind/
|
||||
WandKind/WeaponKind/ArmorKind/TrapKind typed iota enums with Stringer; typed
|
||||
accessors on Object; getItem/inventory/whatis filters take ObjectKind
|
||||
(KindCallable/KindRingOrStick replace CALLABLE/R_OR_S); save format bumped to
|
||||
5.4.4-go2. Suite green.
|
||||
- 2026-07-06 Refactor step 1 (refactor/descriptive-constants): renamed all flag
|
||||
bits, trap types, item subtype constants, and Max* counts to descriptive names
|
||||
(IsHuh→Confused, SeeMonst→SenseMonsters, WsHasteM→WandHasteMonster,
|
||||
MaxSticks→NumWandTypes, ...); Level.NTraps→TrapCount; C names kept as comment
|
||||
breadcrumbs. Pure rename, suite green.
|
||||
- 2026-07-06 Made the rgoue branch Go-only: removed C sources and the
|
||||
autoconf/VS build system (they remain on master and modern-rogue), ported the
|
||||
last wizard command (item-probability listing), rewrote README.md for the Go
|
||||
port (c0b533e)
|
||||
- 2026-07-06 Ported the command loop, save/restore, the tcell terminal layer,
|
||||
and the playable binary at cmd/rogue (41fc104)
|
||||
- 2026-07-06 Ported item effects: potions, scrolls, options, call_it (cdf9bf7)
|
||||
- 2026-07-06 Ported combat, the chase driver, traps, zapping, death and scores
|
||||
(3c5add8)
|
||||
- 2026-07-06 Ported dungeon generation, base items, the pack, and monster
|
||||
creation (a69ef7d)
|
||||
- 2026-07-06 Ported the foundation: types, seed-compatible RNG, item tables,
|
||||
daemon scheduler (7fa2048)
|
||||
- 2026-07-06 Wrote ARCHITECTURE.md Parts 1 and 2: complete map of the C program
|
||||
and the Go port design (91eeee0, 45dba95)
|
||||
- Fork base: Davidslv/rogue C 5.4.4 with modernization fixes (C23 prototypes,
|
||||
ncurses compat), preserved on master/modern-rogue
|
||||
|
||||
# Future Steps
|
||||
|
||||
1. Tag a release once a full game (Amulet retrieval and score entry) completes
|
||||
without defects.
|
||||
2. Full-terminal-size support (deferred by explicit decision 2026-07-06):
|
||||
per-game dungeon dimensions instead of the 80x24 constants; open design
|
||||
questions are resize policy, gameplay tuning at larger sizes, and a --classic
|
||||
80x24 mode.
|
||||
3. 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
|
||||
2026-07-07 request, but do not add a Dockerfile, CI config, or
|
||||
REPO_POLICIES.md.
|
||||
89
armor.c
Normal file
89
armor.c
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* This file contains misc functions for dealing with armor
|
||||
* @(#)armor.c 4.14 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* wear:
|
||||
* The player wants to wear something, so let him/her put it on.
|
||||
*/
|
||||
void
|
||||
wear()
|
||||
{
|
||||
register THING *obj;
|
||||
register char *sp;
|
||||
|
||||
if ((obj = get_item("wear", ARMOR)) == NULL)
|
||||
return;
|
||||
if (cur_armor != NULL)
|
||||
{
|
||||
addmsg("you are already wearing some");
|
||||
if (!terse)
|
||||
addmsg(". You'll have to take it off first");
|
||||
endmsg();
|
||||
after = FALSE;
|
||||
return;
|
||||
}
|
||||
if (obj->o_type != ARMOR)
|
||||
{
|
||||
msg("you can't wear that");
|
||||
return;
|
||||
}
|
||||
waste_time();
|
||||
obj->o_flags |= ISKNOW;
|
||||
sp = inv_name(obj, TRUE);
|
||||
cur_armor = obj;
|
||||
if (!terse)
|
||||
addmsg("you are now ");
|
||||
msg("wearing %s", sp);
|
||||
}
|
||||
|
||||
/*
|
||||
* take_off:
|
||||
* Get the armor off of the players back
|
||||
*/
|
||||
void
|
||||
take_off()
|
||||
{
|
||||
register THING *obj;
|
||||
|
||||
if ((obj = cur_armor) == NULL)
|
||||
{
|
||||
after = FALSE;
|
||||
if (terse)
|
||||
msg("not wearing armor");
|
||||
else
|
||||
msg("you aren't wearing any armor");
|
||||
return;
|
||||
}
|
||||
if (!dropcheck(cur_armor))
|
||||
return;
|
||||
cur_armor = NULL;
|
||||
if (terse)
|
||||
addmsg("was");
|
||||
else
|
||||
addmsg("you used to be");
|
||||
msg(" wearing %c) %s", obj->o_packch, inv_name(obj, TRUE));
|
||||
}
|
||||
|
||||
/*
|
||||
* waste_time:
|
||||
* Do nothing but let other things happen
|
||||
*/
|
||||
void
|
||||
waste_time()
|
||||
{
|
||||
do_daemons(BEFORE);
|
||||
do_fuses(BEFORE);
|
||||
do_daemons(AFTER);
|
||||
do_fuses(AFTER);
|
||||
}
|
||||
541
chase.c
Normal file
541
chase.c
Normal file
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
* Code for one creature to chase another
|
||||
*
|
||||
* @(#)chase.c 4.57 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
#define DRAGONSHOT 5 /* one chance in DRAGONSHOT that a dragon will flame */
|
||||
|
||||
static coord ch_ret; /* Where chasing takes you */
|
||||
|
||||
/*
|
||||
* runners:
|
||||
* Make all the running monsters move.
|
||||
*/
|
||||
void
|
||||
runners()
|
||||
{
|
||||
register THING *tp;
|
||||
THING *next;
|
||||
bool wastarget;
|
||||
static coord orig_pos;
|
||||
|
||||
for (tp = mlist; tp != NULL; tp = next)
|
||||
{
|
||||
/* remember this in case the monster's "next" is changed */
|
||||
next = next(tp);
|
||||
if (!on(*tp, ISHELD) && on(*tp, ISRUN))
|
||||
{
|
||||
orig_pos = tp->t_pos;
|
||||
wastarget = on(*tp, ISTARGET);
|
||||
if (move_monst(tp) == -1)
|
||||
continue;
|
||||
if (on(*tp, ISFLY) && dist_cp(&hero, &tp->t_pos) >= 3)
|
||||
move_monst(tp);
|
||||
if (wastarget && !ce(orig_pos, tp->t_pos))
|
||||
{
|
||||
tp->t_flags &= ~ISTARGET;
|
||||
to_death = FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (has_hit)
|
||||
{
|
||||
endmsg();
|
||||
has_hit = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* move_monst:
|
||||
* Execute a single turn of running for a monster
|
||||
*/
|
||||
int
|
||||
move_monst(THING *tp)
|
||||
{
|
||||
if (!on(*tp, ISSLOW) || tp->t_turn)
|
||||
if (do_chase(tp) == -1)
|
||||
return(-1);
|
||||
if (on(*tp, ISHASTE))
|
||||
if (do_chase(tp) == -1)
|
||||
return(-1);
|
||||
tp->t_turn ^= TRUE;
|
||||
return(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* relocate:
|
||||
* Make the monster's new location be the specified one, updating
|
||||
* all the relevant state.
|
||||
*/
|
||||
void
|
||||
relocate(THING *th, coord *new_loc)
|
||||
{
|
||||
struct room *oroom;
|
||||
|
||||
if (!ce(*new_loc, th->t_pos))
|
||||
{
|
||||
mvaddch(th->t_pos.y, th->t_pos.x, th->t_oldch);
|
||||
th->t_room = roomin(new_loc);
|
||||
set_oldch(th, new_loc);
|
||||
oroom = th->t_room;
|
||||
moat(th->t_pos.y, th->t_pos.x) = NULL;
|
||||
|
||||
if (oroom != th->t_room)
|
||||
th->t_dest = find_dest(th);
|
||||
th->t_pos = *new_loc;
|
||||
moat(new_loc->y, new_loc->x) = th;
|
||||
}
|
||||
move(new_loc->y, new_loc->x);
|
||||
if (see_monst(th))
|
||||
addch(th->t_disguise);
|
||||
else if (on(player, SEEMONST))
|
||||
{
|
||||
standout();
|
||||
addch(th->t_type);
|
||||
standend();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* do_chase:
|
||||
* Make one thing chase another.
|
||||
*/
|
||||
int
|
||||
do_chase(THING *th)
|
||||
{
|
||||
register coord *cp;
|
||||
register struct room *rer, *ree; /* room of chaser, room of chasee */
|
||||
register int mindist = 32767, curdist;
|
||||
register bool stoprun = FALSE; /* TRUE means we are there */
|
||||
register bool door;
|
||||
register THING *obj;
|
||||
static coord this; /* Temporary destination for chaser */
|
||||
|
||||
rer = th->t_room; /* Find room of chaser */
|
||||
if (on(*th, ISGREED) && rer->r_goldval == 0)
|
||||
th->t_dest = &hero; /* If gold has been taken, run after hero */
|
||||
if (th->t_dest == &hero) /* Find room of chasee */
|
||||
ree = proom;
|
||||
else
|
||||
ree = roomin(th->t_dest);
|
||||
/*
|
||||
* We don't count doors as inside rooms for this routine
|
||||
*/
|
||||
door = (chat(th->t_pos.y, th->t_pos.x) == DOOR);
|
||||
/*
|
||||
* If the object of our desire is in a different room,
|
||||
* and we are not in a corridor, run to the door nearest to
|
||||
* our goal.
|
||||
*/
|
||||
over:
|
||||
if (rer != ree)
|
||||
{
|
||||
for (cp = rer->r_exit; cp < &rer->r_exit[rer->r_nexits]; cp++)
|
||||
{
|
||||
curdist = dist_cp(th->t_dest, cp);
|
||||
if (curdist < mindist)
|
||||
{
|
||||
this = *cp;
|
||||
mindist = curdist;
|
||||
}
|
||||
}
|
||||
if (door)
|
||||
{
|
||||
rer = &passages[flat(th->t_pos.y, th->t_pos.x) & F_PNUM];
|
||||
door = FALSE;
|
||||
goto over;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this = *th->t_dest;
|
||||
/*
|
||||
* For dragons check and see if (a) the hero is on a straight
|
||||
* line from it, and (b) that it is within shooting distance,
|
||||
* but outside of striking range.
|
||||
*/
|
||||
if (th->t_type == 'D' && (th->t_pos.y == hero.y || th->t_pos.x == hero.x
|
||||
|| abs(th->t_pos.y - hero.y) == abs(th->t_pos.x - hero.x))
|
||||
&& dist_cp(&th->t_pos, &hero) <= BOLT_LENGTH * BOLT_LENGTH
|
||||
&& !on(*th, ISCANC) && rnd(DRAGONSHOT) == 0)
|
||||
{
|
||||
delta.y = sign(hero.y - th->t_pos.y);
|
||||
delta.x = sign(hero.x - th->t_pos.x);
|
||||
if (has_hit)
|
||||
endmsg();
|
||||
fire_bolt(&th->t_pos, &delta, "flame");
|
||||
running = FALSE;
|
||||
count = 0;
|
||||
quiet = 0;
|
||||
if (to_death && !on(*th, ISTARGET))
|
||||
{
|
||||
to_death = FALSE;
|
||||
kamikaze = FALSE;
|
||||
}
|
||||
return(0);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* This now contains what we want to run to this time
|
||||
* so we run to it. If we hit it we either want to fight it
|
||||
* or stop running
|
||||
*/
|
||||
if (!chase(th, &this))
|
||||
{
|
||||
if (ce(this, hero))
|
||||
{
|
||||
return( attack(th) );
|
||||
}
|
||||
else if (ce(this, *th->t_dest))
|
||||
{
|
||||
for (obj = lvl_obj; obj != NULL; obj = next(obj))
|
||||
if (th->t_dest == &obj->o_pos)
|
||||
{
|
||||
detach(lvl_obj, obj);
|
||||
attach(th->t_pack, obj);
|
||||
chat(obj->o_pos.y, obj->o_pos.x) =
|
||||
(th->t_room->r_flags & ISGONE) ? PASSAGE : FLOOR;
|
||||
th->t_dest = find_dest(th);
|
||||
break;
|
||||
}
|
||||
if (th->t_type != 'F')
|
||||
stoprun = TRUE;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (th->t_type == 'F')
|
||||
return(0);
|
||||
}
|
||||
relocate(th, &ch_ret);
|
||||
/*
|
||||
* And stop running if need be
|
||||
*/
|
||||
if (stoprun && ce(th->t_pos, *(th->t_dest)))
|
||||
th->t_flags &= ~ISRUN;
|
||||
return(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* set_oldch:
|
||||
* Set the oldch character for the monster
|
||||
*/
|
||||
void
|
||||
set_oldch(THING *tp, coord *cp)
|
||||
{
|
||||
char sch;
|
||||
|
||||
if (ce(tp->t_pos, *cp))
|
||||
return;
|
||||
|
||||
sch = tp->t_oldch;
|
||||
tp->t_oldch = CCHAR( mvinch(cp->y,cp->x) );
|
||||
if (!on(player, ISBLIND))
|
||||
{
|
||||
if ((sch == FLOOR || tp->t_oldch == FLOOR) &&
|
||||
(tp->t_room->r_flags & ISDARK))
|
||||
tp->t_oldch = ' ';
|
||||
else if (dist_cp(cp, &hero) <= LAMPDIST && see_floor)
|
||||
tp->t_oldch = chat(cp->y, cp->x);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* see_monst:
|
||||
* Return TRUE if the hero can see the monster
|
||||
*/
|
||||
bool
|
||||
see_monst(THING *mp)
|
||||
{
|
||||
int y, x;
|
||||
|
||||
if (on(player, ISBLIND))
|
||||
return FALSE;
|
||||
if (on(*mp, ISINVIS) && !on(player, CANSEE))
|
||||
return FALSE;
|
||||
y = mp->t_pos.y;
|
||||
x = mp->t_pos.x;
|
||||
if (dist(y, x, hero.y, hero.x) < LAMPDIST)
|
||||
{
|
||||
if (y != hero.y && x != hero.x &&
|
||||
!step_ok(chat(y, hero.x)) && !step_ok(chat(hero.y, x)))
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
if (mp->t_room != proom)
|
||||
return FALSE;
|
||||
return ((bool)!(mp->t_room->r_flags & ISDARK));
|
||||
}
|
||||
|
||||
/*
|
||||
* runto:
|
||||
* Set a monster running after the hero.
|
||||
*/
|
||||
void
|
||||
runto(coord *runner)
|
||||
{
|
||||
register THING *tp;
|
||||
|
||||
/*
|
||||
* If we couldn't find him, something is funny
|
||||
*/
|
||||
#ifdef MASTER
|
||||
if ((tp = moat(runner->y, runner->x)) == NULL)
|
||||
msg("couldn't find monster in runto at (%d,%d)", runner->y, runner->x);
|
||||
#else
|
||||
tp = moat(runner->y, runner->x);
|
||||
#endif
|
||||
/*
|
||||
* Start the beastie running
|
||||
*/
|
||||
tp->t_flags |= ISRUN;
|
||||
tp->t_flags &= ~ISHELD;
|
||||
tp->t_dest = find_dest(tp);
|
||||
}
|
||||
|
||||
/*
|
||||
* chase:
|
||||
* Find the spot for the chaser(er) to move closer to the
|
||||
* chasee(ee). Returns TRUE if we want to keep on chasing later
|
||||
* FALSE if we reach the goal.
|
||||
*/
|
||||
bool
|
||||
chase(THING *tp, coord *ee)
|
||||
{
|
||||
register THING *obj;
|
||||
register int x, y;
|
||||
register int curdist, thisdist;
|
||||
register coord *er = &tp->t_pos;
|
||||
register char ch;
|
||||
register int plcnt = 1;
|
||||
static coord tryp;
|
||||
|
||||
/*
|
||||
* If the thing is confused, let it move randomly. Invisible
|
||||
* Stalkers are slightly confused all of the time, and bats are
|
||||
* quite confused all the time
|
||||
*/
|
||||
if ((on(*tp, ISHUH) && rnd(5) != 0) || (tp->t_type == 'P' && rnd(5) == 0)
|
||||
|| (tp->t_type == 'B' && rnd(2) == 0))
|
||||
{
|
||||
/*
|
||||
* get a valid random move
|
||||
*/
|
||||
ch_ret = *rndmove(tp);
|
||||
curdist = dist_cp(&ch_ret, ee);
|
||||
/*
|
||||
* Small chance that it will become un-confused
|
||||
*/
|
||||
if (rnd(20) == 0)
|
||||
tp->t_flags &= ~ISHUH;
|
||||
}
|
||||
/*
|
||||
* Otherwise, find the empty spot next to the chaser that is
|
||||
* closest to the chasee.
|
||||
*/
|
||||
else
|
||||
{
|
||||
register int ey, ex;
|
||||
/*
|
||||
* This will eventually hold where we move to get closer
|
||||
* If we can't find an empty spot, we stay where we are.
|
||||
*/
|
||||
curdist = dist_cp(er, ee);
|
||||
ch_ret = *er;
|
||||
|
||||
ey = er->y + 1;
|
||||
if (ey >= NUMLINES - 1)
|
||||
ey = NUMLINES - 2;
|
||||
ex = er->x + 1;
|
||||
if (ex >= NUMCOLS)
|
||||
ex = NUMCOLS - 1;
|
||||
|
||||
for (x = er->x - 1; x <= ex; x++)
|
||||
{
|
||||
if (x < 0)
|
||||
continue;
|
||||
tryp.x = x;
|
||||
for (y = er->y - 1; y <= ey; y++)
|
||||
{
|
||||
tryp.y = y;
|
||||
if (!diag_ok(er, &tryp))
|
||||
continue;
|
||||
ch = winat(y, x);
|
||||
if (step_ok(ch))
|
||||
{
|
||||
/*
|
||||
* If it is a scroll, it might be a scare monster scroll
|
||||
* so we need to look it up to see what type it is.
|
||||
*/
|
||||
if (ch == SCROLL)
|
||||
{
|
||||
for (obj = lvl_obj; obj != NULL; obj = next(obj))
|
||||
{
|
||||
if (y == obj->o_pos.y && x == obj->o_pos.x)
|
||||
break;
|
||||
}
|
||||
if (obj != NULL && obj->o_which == S_SCARE)
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* It can also be a Xeroc, which we shouldn't step on
|
||||
*/
|
||||
if ((obj = moat(y, x)) != NULL && obj->t_type == 'X')
|
||||
continue;
|
||||
/*
|
||||
* If we didn't find any scrolls at this place or it
|
||||
* wasn't a scare scroll, then this place counts
|
||||
*/
|
||||
thisdist = dist(y, x, ee->y, ee->x);
|
||||
if (thisdist < curdist)
|
||||
{
|
||||
plcnt = 1;
|
||||
ch_ret = tryp;
|
||||
curdist = thisdist;
|
||||
}
|
||||
else if (thisdist == curdist && rnd(++plcnt) == 0)
|
||||
{
|
||||
ch_ret = tryp;
|
||||
curdist = thisdist;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (bool)(curdist != 0 && !ce(ch_ret, hero));
|
||||
}
|
||||
|
||||
/*
|
||||
* roomin:
|
||||
* Find what room some coordinates are in. NULL means they aren't
|
||||
* in any room.
|
||||
*/
|
||||
struct room *
|
||||
roomin(coord *cp)
|
||||
{
|
||||
register struct room *rp;
|
||||
register char *fp;
|
||||
|
||||
|
||||
fp = &flat(cp->y, cp->x);
|
||||
if (*fp & F_PASS)
|
||||
return &passages[*fp & F_PNUM];
|
||||
|
||||
for (rp = rooms; rp < &rooms[MAXROOMS]; rp++)
|
||||
if (cp->x <= rp->r_pos.x + rp->r_max.x && rp->r_pos.x <= cp->x
|
||||
&& cp->y <= rp->r_pos.y + rp->r_max.y && rp->r_pos.y <= cp->y)
|
||||
return rp;
|
||||
|
||||
msg("in some bizarre place (%d, %d)", unc(*cp));
|
||||
#ifdef MASTER
|
||||
abort();
|
||||
return NULL;
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* diag_ok:
|
||||
* Check to see if the move is legal if it is diagonal
|
||||
*/
|
||||
bool
|
||||
diag_ok(coord *sp, coord *ep)
|
||||
{
|
||||
if (ep->x < 0 || ep->x >= NUMCOLS || ep->y <= 0 || ep->y >= NUMLINES - 1)
|
||||
return FALSE;
|
||||
if (ep->x == sp->x || ep->y == sp->y)
|
||||
return TRUE;
|
||||
return (bool)(step_ok(chat(ep->y, sp->x)) && step_ok(chat(sp->y, ep->x)));
|
||||
}
|
||||
|
||||
/*
|
||||
* cansee:
|
||||
* Returns true if the hero can see a certain coordinate.
|
||||
*/
|
||||
bool
|
||||
cansee(int y, int x)
|
||||
{
|
||||
register struct room *rer;
|
||||
static coord tp;
|
||||
|
||||
if (on(player, ISBLIND))
|
||||
return FALSE;
|
||||
if (dist(y, x, hero.y, hero.x) < LAMPDIST)
|
||||
{
|
||||
if (flat(y, x) & F_PASS)
|
||||
if (y != hero.y && x != hero.x &&
|
||||
!step_ok(chat(y, hero.x)) && !step_ok(chat(hero.y, x)))
|
||||
return FALSE;
|
||||
return TRUE;
|
||||
}
|
||||
/*
|
||||
* We can only see if the hero in the same room as
|
||||
* the coordinate and the room is lit or if it is close.
|
||||
*/
|
||||
tp.y = y;
|
||||
tp.x = x;
|
||||
return (bool)((rer = roomin(&tp)) == proom && !(rer->r_flags & ISDARK));
|
||||
}
|
||||
|
||||
/*
|
||||
* find_dest:
|
||||
* find the proper destination for the monster
|
||||
*/
|
||||
coord *
|
||||
find_dest(THING *tp)
|
||||
{
|
||||
register THING *obj;
|
||||
register int prob;
|
||||
|
||||
if ((prob = monsters[tp->t_type - 'A'].m_carry) <= 0 || tp->t_room == proom
|
||||
|| see_monst(tp))
|
||||
return &hero;
|
||||
for (obj = lvl_obj; obj != NULL; obj = next(obj))
|
||||
{
|
||||
if (obj->o_type == SCROLL && obj->o_which == S_SCARE)
|
||||
continue;
|
||||
if (roomin(&obj->o_pos) == tp->t_room && rnd(100) < prob)
|
||||
{
|
||||
for (tp = mlist; tp != NULL; tp = next(tp))
|
||||
if (tp->t_dest == &obj->o_pos)
|
||||
break;
|
||||
if (tp == NULL)
|
||||
return &obj->o_pos;
|
||||
}
|
||||
}
|
||||
return &hero;
|
||||
}
|
||||
|
||||
/*
|
||||
* dist:
|
||||
* Calculate the "distance" between to points. Actually,
|
||||
* this calculates d^2, not d, but that's good enough for
|
||||
* our purposes, since it's only used comparitively.
|
||||
*/
|
||||
int
|
||||
dist(int y1, int x1, int y2, int x2)
|
||||
{
|
||||
return ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
|
||||
}
|
||||
|
||||
/*
|
||||
* dist_cp:
|
||||
* Call dist() with appropriate arguments for coord pointers
|
||||
*/
|
||||
int
|
||||
dist_cp(coord *c1, coord *c2)
|
||||
{
|
||||
return dist(c1->y, c1->x, c2->y, c2->x);
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
// 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"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/rgoue/game"
|
||||
"git.eeqj.de/sneak/rgoue/term"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(run())
|
||||
}
|
||||
|
||||
// run does the real work and returns an exit code. It only returns on a
|
||||
// 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 {
|
||||
scores := flag.Bool("s", false, "print the scoreboard and exit")
|
||||
deathDemo := flag.Bool("d", false, "die a random death (demo)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
params := loadParams()
|
||||
|
||||
if *scores {
|
||||
game.New(params).ShowScores()
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// C printed its greeting just before initscr(); here that means just
|
||||
// before the tcell screen takes the terminal, and on stdout, exactly
|
||||
// as C did (main.c main). C followed the printf with fflush because
|
||||
// its stdout was buffered; os.Stdout is not, so the write is the
|
||||
// flush.
|
||||
if digsNewDungeon(*deathDemo, flag.Args()) {
|
||||
_, _ = fmt.Fprint(os.Stdout, game.Greeting(params)) // CLI output
|
||||
}
|
||||
|
||||
t, err := term.New()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
|
||||
return 1
|
||||
}
|
||||
defer t.Fini()
|
||||
|
||||
// Armed here, the instant the tty goes raw, not after the game is
|
||||
// built: everything below this line — the restore, the death demo
|
||||
// (which never returns), the game itself — would otherwise run raw
|
||||
// with no handler installed. There is no game to save yet, so the
|
||||
// saver is filled in below once there is one.
|
||||
pending := installSignalHandlers(t)
|
||||
|
||||
params.Term = t
|
||||
|
||||
var g *game.RogueGame
|
||||
|
||||
if args := flag.Args(); len(args) == 1 && !*deathDemo {
|
||||
// restore a saved game
|
||||
g, err = game.Restore(args[0], params)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err) // deferred Fini restores the terminal
|
||||
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
g = game.New(params)
|
||||
}
|
||||
|
||||
if *deathDemo {
|
||||
// The demo is left without a saver on purpose: a signal still
|
||||
// restores the terminal, but a throwaway demo game is not worth
|
||||
// writing over the player's save file.
|
||||
g.DeathDemo() // does not return: death exits the process
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
pending.set(g)
|
||||
|
||||
g.Run() // does not return: the game ends by exiting the process
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// digsNewDungeon reports whether this invocation is the one that digs a
|
||||
// fresh dungeon, and so the only one that greets.
|
||||
//
|
||||
// C's printf is the last statement before initscr(), and everything that
|
||||
// does something else has already left by then: -s scores and exits, -d
|
||||
// runs the death demo and exits, and restore() — the argc == 2 case that
|
||||
// is neither — never returns. So a saved game resumes without a greeting,
|
||||
// which is right: nothing is being dug.
|
||||
//
|
||||
// The restore test is duplicated from run's own, deliberately. Keeping
|
||||
// them as one predicate would mean deciding the startup path before the
|
||||
// terminal exists and carrying it past the error returns, which is more
|
||||
// rearrangement of run than a greeting is worth.
|
||||
func digsNewDungeon(deathDemo bool, args []string) bool {
|
||||
return !deathDemo && len(args) != 1
|
||||
}
|
||||
|
||||
// loadParams gathers the game parameters from the environment: home
|
||||
// directory, ROGUEOPTS, user name, wizard mode, and the dungeon seed
|
||||
// (main.c's startup).
|
||||
func loadParams() game.Params {
|
||||
home, _ := os.UserHomeDir()
|
||||
|
||||
name := ""
|
||||
|
||||
u, userErr := user.Current()
|
||||
if userErr == nil {
|
||||
name = u.Username
|
||||
}
|
||||
|
||||
wizard := os.Getenv("ROGUE_WIZARD") != ""
|
||||
|
||||
return game.Params{
|
||||
Seed: chooseSeed(wizard),
|
||||
Name: name,
|
||||
RogueOpts: os.Getenv("ROGUEOPTS"),
|
||||
Home: home,
|
||||
ScorePath: home + "/.rogue.scores",
|
||||
Wizard: wizard,
|
||||
}
|
||||
}
|
||||
|
||||
// saver is the autosave half of *game.RogueGame that the signal handler
|
||||
// needs; an interface so the handler is testable headlessly.
|
||||
type saver interface {
|
||||
// AutoSaveOnSignal asks the game goroutine to write the save file and
|
||||
// waits up to timeout for it, reporting whether the save ran (save.c
|
||||
// auto_save). The handler never encodes anything itself; see
|
||||
// signalSaveTimeout.
|
||||
AutoSaveOnSignal(timeout time.Duration) bool
|
||||
}
|
||||
|
||||
// signalSaveTimeout bounds how long the signal handler waits for the game
|
||||
// goroutine to take its autosave.
|
||||
//
|
||||
// The handler cannot encode the game itself — that was issue #24's data
|
||||
// race — so it has to hand the work to the goroutine that owns the state
|
||||
// and wait. The game answers between turns, while parked waiting for a
|
||||
// key, and while parked in the shell escape, which covers everywhere it
|
||||
// can sit for any length of time; the deadline is the backstop for a game
|
||||
// goroutine wedged somewhere with no service point, so that a signal can
|
||||
// never fail to get the process out. It is generous next to the
|
||||
// milliseconds a gob encode of one game takes, and invisible to a player
|
||||
// whose connection has already dropped.
|
||||
//
|
||||
// Giving up costs nothing now that saveFile renames over the target
|
||||
// (game/save.go): a save that does not happen leaves the previous save
|
||||
// whole, where the old remove-then-encode could leave the player with
|
||||
// neither.
|
||||
const signalSaveTimeout = 3 * time.Second
|
||||
|
||||
// finisher is the terminal-restoring half of game.Terminal that the
|
||||
// signal handler needs (curses endwin).
|
||||
type finisher interface {
|
||||
// Fini restores the terminal to its pre-game state.
|
||||
Fini()
|
||||
}
|
||||
|
||||
// pendingSaver is the saver the signal handler holds from the moment the
|
||||
// terminal goes raw. The handler has to be armed before there is a game
|
||||
// to save — restoring a save file and the death demo both run with the
|
||||
// tty already raw — so AutoSaveOnSignal does nothing until set hands over
|
||||
// the real game. The mutex is not decoration: set runs on the main
|
||||
// goroutine and AutoSaveOnSignal on the signal goroutine.
|
||||
type pendingSaver struct {
|
||||
mu sync.Mutex
|
||||
game saver
|
||||
}
|
||||
|
||||
// AutoSaveOnSignal saves the game if there is one yet, and otherwise does
|
||||
// nothing: a signal arriving before the game is built still restores the
|
||||
// terminal, which is the part that matters.
|
||||
//
|
||||
// The lock is held only long enough to read the game, not across the
|
||||
// delegated save. That changed with issue #24: the real
|
||||
// AutoSaveOnSignal now blocks until the game goroutine takes the save or
|
||||
// the deadline expires, and holding the mutex across a wait that long
|
||||
// would stall a concurrent set — the case the PR #23 review flagged as
|
||||
// safe only for as long as set is called exactly once. This shape does
|
||||
// not depend on that.
|
||||
func (p *pendingSaver) AutoSaveOnSignal(timeout time.Duration) bool {
|
||||
p.mu.Lock()
|
||||
g := p.game
|
||||
p.mu.Unlock()
|
||||
|
||||
if g == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return g.AutoSaveOnSignal(timeout)
|
||||
}
|
||||
|
||||
// set hands the signal handler the game to autosave, once one exists.
|
||||
func (p *pendingSaver) set(g saver) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.game = g
|
||||
}
|
||||
|
||||
// handledSignals returns the signals the game leaves on. They split into
|
||||
// two groups with deliberately different save behavior; see
|
||||
// savesOnSignal.
|
||||
func handledSignals() []os.Signal {
|
||||
return []os.Signal{
|
||||
syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT,
|
||||
}
|
||||
}
|
||||
|
||||
// savesOnSignal reports whether the game should autosave on its way out
|
||||
// for this signal.
|
||||
//
|
||||
// THE DECISION (issue #12): SIGHUP and SIGTERM save; SIGINT and SIGQUIT
|
||||
// restore the terminal and exit WITHOUT saving. This is deliberate, not
|
||||
// an oversight, on three grounds.
|
||||
//
|
||||
// C: no path in the C game saves on INT or QUIT. The shipped build
|
||||
// installs no handler at all during play (mach_dep.c setup calls
|
||||
// md_onsignal_default), and the only INT handler it ever installs is
|
||||
// rip.c/main.c's leave() in the endgame — endwin and exit, explicitly
|
||||
// discarding pending output. The build that does wire INT during play
|
||||
// (md_onsignal_autosave, mdport.c — defined unconditionally, but with
|
||||
// its only call site, mach_dep.c setup, inside #ifdef DUMP) sends it to
|
||||
// quit(), which confirms, scores, and exits, again without saving, and
|
||||
// sends QUIT to endit() -> fatal() -> endwin + exit. save.c auto_save is
|
||||
// reserved for HUP/TERM. Saving on HUP/TERM but not on INT/QUIT is
|
||||
// therefore exactly C's split.
|
||||
//
|
||||
// Semantics: HUP and TERM mean involuntary teardown — the line dropped
|
||||
// or the machine is going down — so rescuing the game is right. INT and
|
||||
// QUIT are the player deliberately saying "stop now". Rogue scores a
|
||||
// deliberate quit, and its save discipline is anti-save-scum by design
|
||||
// (restoring consumes the file), so making Ctrl-C a free checkpoint
|
||||
// would turn it into a one-keystroke undo for a bad turn: a gameplay
|
||||
// change, not a robustness fix.
|
||||
//
|
||||
// Safety: this used to be the third ground, back when the handler
|
||||
// gob-encoded live game state from its own goroutine after removing the
|
||||
// save file — a data race with a window in which the player had no save
|
||||
// at all, accepted on HUP/TERM because the process was dying anyway and
|
||||
// avoided entirely on INT/QUIT. Issue #24 removed the window instead of
|
||||
// living with it: the handler now hands the save to the game goroutine
|
||||
// and waits (AutoSaveOnSignal), and the write goes to a temporary file
|
||||
// renamed over the target. The split above stands on C and on semantics,
|
||||
// which is where it always belonged; INT and QUIT do not save because
|
||||
// the player asked to stop, not because saving is dangerous.
|
||||
func savesOnSignal(sig os.Signal) bool {
|
||||
return sig == syscall.SIGHUP || sig == syscall.SIGTERM
|
||||
}
|
||||
|
||||
// installSignalHandlers arranges for the game to leave the terminal
|
||||
// usable when it is signalled: C's leave(), "leave quickly but
|
||||
// curteously" (main.c), extended with save.c auto_save on the two
|
||||
// signals that warrant it.
|
||||
//
|
||||
// Call it the instant the terminal goes raw, which is earlier than the
|
||||
// game exists; the returned pendingSaver takes the game once it does.
|
||||
// Restoring the terminal is what has to be armed the moment the tty
|
||||
// stops being usable, and it does not need a game.
|
||||
func installSignalHandlers(t finisher) *pendingSaver {
|
||||
pending := &pendingSaver{}
|
||||
|
||||
go leaveOnSignal(notifySignals(), pending, t, os.Exit)
|
||||
|
||||
return pending
|
||||
}
|
||||
|
||||
// notifySignals subscribes to the handled signals and returns the
|
||||
// channel they arrive on. Split out from installSignalHandlers so tests
|
||||
// can drive leaveOnSignal with real signal delivery.
|
||||
func notifySignals() chan os.Signal {
|
||||
// Buffered so signal delivery never blocks, and deliberately never
|
||||
// drained past the first signal: see leaveOnSignal.
|
||||
sig := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sig, handledSignals()...)
|
||||
|
||||
return sig
|
||||
}
|
||||
|
||||
// leaveOnSignal waits for one signal and takes the game out.
|
||||
//
|
||||
// Exactly one goroutine reads exactly one signal, which is what makes
|
||||
// the exit safe: a second signal (a SIGINT landing while a SIGHUP's save
|
||||
// is still being written, say) stays in the buffer unread and can never
|
||||
// call exit out from under an in-flight save. The order within is the
|
||||
// same one myExit uses (game/rip.go): save if this signal saves, then
|
||||
// restore the terminal, then exit.
|
||||
//
|
||||
// AutoSaveOnSignal returns once the game goroutine has finished writing,
|
||||
// or once signalSaveTimeout has run out, so the save is complete before
|
||||
// the terminal is torn down and the process leaves — and the process
|
||||
// leaves either way.
|
||||
func leaveOnSignal(sig <-chan os.Signal, g saver, t finisher, exit func(int)) {
|
||||
if savesOnSignal(<-sig) {
|
||||
g.AutoSaveOnSignal(signalSaveTimeout)
|
||||
}
|
||||
|
||||
t.Fini()
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// chooseSeed picks the dungeon number: SEED for reproducible dungeons
|
||||
// (wizard mode, as in the C game), else time+pid (main.c).
|
||||
func chooseSeed(wizard bool) int32 {
|
||||
if env := os.Getenv("SEED"); env != "" && wizard {
|
||||
n, err := strconv.ParseInt(env, 10, 32)
|
||||
if err == nil {
|
||||
return int32(n)
|
||||
}
|
||||
}
|
||||
|
||||
// The C game computed `lowtime + getpid()` in int; the truncation to
|
||||
// 32 bits is the same wraparound the C int arithmetic performed.
|
||||
//nolint:mnd // C-faithful: the C int wraparound mask
|
||||
return int32(time.Now().Unix()&0x7fffffff) +
|
||||
int32(os.Getpid()&0x7fffffff)
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
package main
|
||||
|
||||
// White-box tests for the signal plumbing. Unlike the game package's test
|
||||
// files this one carries no //nolint:testpackage directive: testpackage
|
||||
// exempts package main, so nolintlint rejects the directive as unused.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The steps the signal handler can take, in the order signalRecorder
|
||||
// records them.
|
||||
const (
|
||||
stepSave = "save"
|
||||
stepFini = "fini"
|
||||
stepExit = "exit"
|
||||
)
|
||||
|
||||
// wantHandledSignals is the exact set of signals the game must leave on.
|
||||
// This is the subject of issue #12: SIGHUP and SIGTERM were handled and
|
||||
// SIGINT and SIGQUIT were not, so the latter two killed the process with
|
||||
// the tty still raw. Every other test here iterates handledSignals(), so
|
||||
// without this one the whole file would pass against a set that had
|
||||
// silently lost SIGINT and SIGQUIT again.
|
||||
func wantHandledSignals() []os.Signal {
|
||||
return []os.Signal{
|
||||
syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT,
|
||||
}
|
||||
}
|
||||
|
||||
// wantSteps is the expected handler step sequence for each handled
|
||||
// signal, and the single source of truth for the tests that check the
|
||||
// save/no-save split.
|
||||
func wantSteps() map[os.Signal][]string {
|
||||
return map[os.Signal][]string{
|
||||
syscall.SIGHUP: {stepSave, stepFini, stepExit},
|
||||
syscall.SIGTERM: {stepSave, stepFini, stepExit},
|
||||
syscall.SIGINT: {stepFini, stepExit},
|
||||
syscall.SIGQUIT: {stepFini, stepExit},
|
||||
}
|
||||
}
|
||||
|
||||
// signalRecorder stands in for the game and the terminal in the signal
|
||||
// handler, recording the order of the steps the handler takes. The mutex
|
||||
// matters: the handler runs on its own goroutine, so an unguarded slice
|
||||
// would be a data race under -race, which is exactly what these tests
|
||||
// are meant to rule out.
|
||||
type signalRecorder struct {
|
||||
mu sync.Mutex
|
||||
steps []string
|
||||
code int
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newSignalRecorder() *signalRecorder {
|
||||
return &signalRecorder{done: make(chan struct{})}
|
||||
}
|
||||
|
||||
// AutoSaveOnSignal records a save attempt (the saver half). The real one
|
||||
// hands the work to the game goroutine and waits; the recorder stands in
|
||||
// for a game that takes it immediately.
|
||||
func (r *signalRecorder) AutoSaveOnSignal(time.Duration) bool {
|
||||
r.record(stepSave)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Fini records a terminal restore (the finisher half).
|
||||
func (r *signalRecorder) Fini() {
|
||||
r.record(stepFini)
|
||||
}
|
||||
|
||||
// exit records the process exit that ends the handler and releases any
|
||||
// waiter. It stands in for os.Exit, which cannot be called in a test.
|
||||
func (r *signalRecorder) exit(code int) {
|
||||
r.mu.Lock()
|
||||
r.code = code
|
||||
r.steps = append(r.steps, stepExit)
|
||||
r.mu.Unlock()
|
||||
|
||||
close(r.done)
|
||||
}
|
||||
|
||||
// record appends one step.
|
||||
func (r *signalRecorder) record(step string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.steps = append(r.steps, step)
|
||||
}
|
||||
|
||||
// taken returns the recorded steps and the exit code.
|
||||
func (r *signalRecorder) taken() ([]string, int) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
return slices.Clone(r.steps), r.code
|
||||
}
|
||||
|
||||
// TestHandledSignalsSet pins the membership of handledSignals() itself.
|
||||
// The regression issue #12 exists to prevent is a signal dropping out of
|
||||
// that set — SIGINT and SIGQUIT reaching the process at SIG_DFL and
|
||||
// killing it with the tty raw — and every other test in this file is
|
||||
// driven by the set, so only this test can fail on it.
|
||||
func TestHandledSignalsSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := handledSignals()
|
||||
want := wantHandledSignals()
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Errorf("handledSignals() = %v, want exactly %v", got, want)
|
||||
}
|
||||
|
||||
for _, sig := range want {
|
||||
if !slices.Contains(got, sig) {
|
||||
t.Errorf("handledSignals() = %v, missing %v", got, sig)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sig := range got {
|
||||
if !slices.Contains(want, sig) {
|
||||
t.Errorf("handledSignals() = %v, unexpected %v", got, sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLeaveOnSignalRestoresTerminalBeforeExit is the core of issue #12:
|
||||
// whatever the signal, the terminal is restored before the process ends,
|
||||
// so the player is never dropped into a shell with the tty still in raw
|
||||
// mode.
|
||||
func TestLeaveOnSignalRestoresTerminalBeforeExit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, sig := range handledSignals() {
|
||||
rec := newSignalRecorder()
|
||||
|
||||
ch := make(chan os.Signal, 1)
|
||||
ch <- sig
|
||||
|
||||
leaveOnSignal(ch, rec, rec, rec.exit)
|
||||
|
||||
steps, code := rec.taken()
|
||||
|
||||
fini := slices.Index(steps, stepFini)
|
||||
exit := slices.Index(steps, stepExit)
|
||||
|
||||
if fini < 0 || exit < 0 || fini > exit {
|
||||
t.Errorf("%v: want the terminal restored before exit, got %v",
|
||||
sig, steps)
|
||||
}
|
||||
|
||||
if code != 0 {
|
||||
t.Errorf("%v: exit code = %d, want 0", sig, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLeaveOnSignalSaveSplit pins the decision recorded on savesOnSignal:
|
||||
// SIGHUP/SIGTERM (involuntary teardown) save on the way out, SIGINT and
|
||||
// SIGQUIT (a deliberate "stop now" from the player) do not, matching C,
|
||||
// where auto_save is reserved for HUP/TERM and neither leave() nor quit()
|
||||
// nor endit() writes a save file.
|
||||
//
|
||||
// It is driven by the expectation table rather than by
|
||||
// handledSignals(), so that every entry — including the SIGINT and
|
||||
// SIGQUIT ones — is actually read, and a signal dropped from the handled
|
||||
// set fails here as well as in TestHandledSignalsSet.
|
||||
func TestLeaveOnSignalSaveSplit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for sig, want := range wantSteps() {
|
||||
if !slices.Contains(handledSignals(), sig) {
|
||||
t.Errorf("%v is not handled at all, so it cannot exit cleanly", sig)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
rec := newSignalRecorder()
|
||||
|
||||
ch := make(chan os.Signal, 1)
|
||||
ch <- sig
|
||||
|
||||
leaveOnSignal(ch, rec, rec, rec.exit)
|
||||
|
||||
steps, _ := rec.taken()
|
||||
if !slices.Equal(steps, want) {
|
||||
t.Errorf("%v: steps = %v, want %v", sig, steps, want)
|
||||
}
|
||||
|
||||
if saved := slices.Contains(steps, stepSave); saved != savesOnSignal(sig) {
|
||||
t.Errorf("%v: saved = %v, savesOnSignal = %v",
|
||||
sig, saved, savesOnSignal(sig))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLeaveOnSignalIgnoresLaterSignals covers the ordering guarantee in
|
||||
// leaveOnSignal's comment: only the first signal is read, so a second one
|
||||
// arriving mid-save cannot exit out from under the save and truncate the
|
||||
// player's file. The saver here blocks until a second signal has been
|
||||
// queued, reproducing that window.
|
||||
func TestLeaveOnSignalIgnoresLaterSignals(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := newSignalRecorder()
|
||||
|
||||
ch := make(chan os.Signal, 2)
|
||||
ch <- syscall.SIGHUP
|
||||
|
||||
blocker := &blockingSaver{rec: rec, queue: ch, extra: syscall.SIGINT}
|
||||
|
||||
leaveOnSignal(ch, blocker, rec, rec.exit)
|
||||
|
||||
steps, _ := rec.taken()
|
||||
if !slices.Equal(steps, []string{stepSave, stepFini, stepExit}) {
|
||||
t.Errorf("steps = %v, want one save, one fini, one exit", steps)
|
||||
}
|
||||
|
||||
if len(ch) != 1 {
|
||||
t.Errorf("queued signals left unread = %d, want 1", len(ch))
|
||||
}
|
||||
}
|
||||
|
||||
// blockingSaver queues another signal while the save is in flight, the
|
||||
// race window leaveOnSignal is built to close.
|
||||
type blockingSaver struct {
|
||||
rec *signalRecorder
|
||||
queue chan os.Signal
|
||||
extra os.Signal
|
||||
}
|
||||
|
||||
// AutoSaveOnSignal delivers the extra signal mid-save, then records the
|
||||
// save.
|
||||
func (b *blockingSaver) AutoSaveOnSignal(timeout time.Duration) bool {
|
||||
b.queue <- b.extra
|
||||
|
||||
return b.rec.AutoSaveOnSignal(timeout)
|
||||
}
|
||||
|
||||
// TestLeaveOnRealSignal is the deepest headless check available: it
|
||||
// delivers real SIGINT/SIGQUIT/SIGHUP/SIGTERM to this process through
|
||||
// os/signal, exactly as notifySignals wires them in the game, and
|
||||
// verifies each one reaches the handler and produces the full expected
|
||||
// step sequence — including the save/no-save split, which this test is
|
||||
// the best placed to check end to end.
|
||||
//
|
||||
// What cannot be checked here is the tty itself coming back out of raw
|
||||
// mode: that needs a controlling terminal and a live tcell screen, which
|
||||
// a headless test run does not have. This test covers everything up to
|
||||
// the Terminal.Fini call; term.Tcell.Fini is a direct pass-through to
|
||||
// tcell's Screen.Fini, which is the same call myExit already relies on.
|
||||
func TestLeaveOnRealSignal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ch := notifySignals()
|
||||
defer signal.Stop(ch)
|
||||
|
||||
for _, sig := range handledSignals() {
|
||||
rec := newSignalRecorder()
|
||||
|
||||
go leaveOnSignal(ch, rec, rec, rec.exit)
|
||||
|
||||
unix, ok := sig.(syscall.Signal)
|
||||
if !ok {
|
||||
t.Fatalf("%v is not a unix signal", sig)
|
||||
}
|
||||
|
||||
err := syscall.Kill(os.Getpid(), unix)
|
||||
if err != nil {
|
||||
t.Fatalf("kill(%v): %v", sig, err)
|
||||
}
|
||||
|
||||
<-rec.done
|
||||
|
||||
steps, code := rec.taken()
|
||||
if want := wantSteps()[sig]; !slices.Equal(steps, want) {
|
||||
t.Errorf("%v: steps = %v, want %v", sig, steps, want)
|
||||
}
|
||||
|
||||
if code != 0 {
|
||||
t.Errorf("%v: exit code = %d, want 0", sig, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPendingSaverArmsBeforeTheGameExists covers what lets the handlers
|
||||
// be installed the instant the terminal goes raw rather than after the
|
||||
// game is built: a signal arriving before there is a game must still
|
||||
// reach Fini, and must not save anything, while one arriving after the
|
||||
// game is handed over saves it.
|
||||
func TestPendingSaverArmsBeforeTheGameExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pending := &pendingSaver{}
|
||||
rec := newSignalRecorder()
|
||||
|
||||
ch := make(chan os.Signal, 1)
|
||||
ch <- syscall.SIGHUP
|
||||
|
||||
// No game yet: the SIGHUP still restores the terminal and exits, it
|
||||
// just has nothing to write.
|
||||
leaveOnSignal(ch, pending, rec, rec.exit)
|
||||
|
||||
steps, code := rec.taken()
|
||||
if want := []string{stepFini, stepExit}; !slices.Equal(steps, want) {
|
||||
t.Errorf("before the game exists: steps = %v, want %v", steps, want)
|
||||
}
|
||||
|
||||
if code != 0 {
|
||||
t.Errorf("before the game exists: exit code = %d, want 0", code)
|
||||
}
|
||||
|
||||
// Once the game is handed over, the same saver writes it.
|
||||
started := newSignalRecorder()
|
||||
pending.set(started)
|
||||
|
||||
if !pending.AutoSaveOnSignal(signalSaveTimeout) {
|
||||
t.Error("after set: the save was not reported as taken")
|
||||
}
|
||||
|
||||
saved, _ := started.taken()
|
||||
if want := []string{stepSave}; !slices.Equal(saved, want) {
|
||||
t.Errorf("after set: steps = %v, want %v", saved, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPendingSaverDoesNotHoldItsLockAcrossTheSave pins the reason
|
||||
// pendingSaver reads the game out from under the mutex instead of
|
||||
// delegating with it held: since issue #24 the delegated save blocks
|
||||
// until the game goroutine takes it or the deadline expires, so a mutex
|
||||
// held across it would stall whoever calls set. Nothing calls set twice
|
||||
// today, which is why the PR #23 review recorded this as a future-proof
|
||||
// note rather than a bug — this test is what stops it becoming one.
|
||||
func TestPendingSaverDoesNotHoldItsLockAcrossTheSave(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pending := &pendingSaver{}
|
||||
stuck := &stuckSaver{entered: make(chan struct{}), release: make(chan struct{})}
|
||||
pending.set(stuck)
|
||||
|
||||
go pending.AutoSaveOnSignal(signalSaveTimeout)
|
||||
|
||||
<-stuck.entered // the delegated save is in flight
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
pending.set(newSignalRecorder()) // must not block on the save
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Error("set blocked while a save was in flight: the lock is held across it")
|
||||
}
|
||||
|
||||
close(stuck.release)
|
||||
}
|
||||
|
||||
// stuckSaver blocks inside the delegated save until it is released,
|
||||
// standing in for a game goroutine that is slow to answer.
|
||||
type stuckSaver struct {
|
||||
entered chan struct{}
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
// AutoSaveOnSignal blocks until the test releases it.
|
||||
func (s *stuckSaver) AutoSaveOnSignal(time.Duration) bool {
|
||||
close(s.entered)
|
||||
<-s.release
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// TestDigsNewDungeon pins which invocations reach C's greeting. In main.c
|
||||
// the printf is the last statement before initscr(), so -s and -d, which
|
||||
// exit earlier, never see it, and neither does a restored game, because
|
||||
// restore() does not return. The saved-game case is the one worth having
|
||||
// a test for: resuming a dungeon must not announce that one is being dug.
|
||||
func TestDigsNewDungeon(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
deathDemo bool
|
||||
args []string
|
||||
want bool
|
||||
}{
|
||||
{name: "new game", args: nil, want: true},
|
||||
{name: "restore a save", args: []string{"rogue.save"}, want: false},
|
||||
{name: "death demo", deathDemo: true, want: false},
|
||||
{
|
||||
name: "death demo wins over a save argument",
|
||||
deathDemo: true,
|
||||
args: []string{"rogue.save"},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := digsNewDungeon(tc.deathDemo, tc.args); got != tc.want {
|
||||
t.Errorf("digsNewDungeon(%v, %v) = %v, want %v",
|
||||
tc.deathDemo, tc.args, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
820
command.c
Normal file
820
command.c
Normal file
@@ -0,0 +1,820 @@
|
||||
/*
|
||||
* Read and execute the user commands
|
||||
*
|
||||
* @(#)command.c 4.73 (Berkeley) 08/06/83
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curses.h>
|
||||
#include <ctype.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* command:
|
||||
* Process the user commands
|
||||
*/
|
||||
void
|
||||
command()
|
||||
{
|
||||
register char ch;
|
||||
register int ntimes = 1; /* Number of player moves */
|
||||
char *fp;
|
||||
THING *mp;
|
||||
static char countch, direction, newcount = FALSE;
|
||||
|
||||
if (on(player, ISHASTE))
|
||||
ntimes++;
|
||||
/*
|
||||
* Let the daemons start up
|
||||
*/
|
||||
do_daemons(BEFORE);
|
||||
do_fuses(BEFORE);
|
||||
while (ntimes--)
|
||||
{
|
||||
again = FALSE;
|
||||
if (has_hit)
|
||||
{
|
||||
endmsg();
|
||||
has_hit = FALSE;
|
||||
}
|
||||
/*
|
||||
* these are illegal things for the player to be, so if any are
|
||||
* set, someone's been poking in memeory
|
||||
*/
|
||||
if (on(player, ISSLOW|ISGREED|ISINVIS|ISREGEN|ISTARGET))
|
||||
exit(1);
|
||||
|
||||
look(TRUE);
|
||||
if (!running)
|
||||
door_stop = FALSE;
|
||||
status();
|
||||
lastscore = purse;
|
||||
move(hero.y, hero.x);
|
||||
if (!((running || count) && jump))
|
||||
refresh(); /* Draw screen */
|
||||
take = 0;
|
||||
after = TRUE;
|
||||
/*
|
||||
* Read command or continue run
|
||||
*/
|
||||
#ifdef MASTER
|
||||
if (wizard)
|
||||
noscore = TRUE;
|
||||
#endif
|
||||
if (!no_command)
|
||||
{
|
||||
if (running || to_death)
|
||||
ch = runch;
|
||||
else if (count)
|
||||
ch = countch;
|
||||
else
|
||||
{
|
||||
ch = readchar();
|
||||
move_on = FALSE;
|
||||
if (mpos != 0) /* Erase message if its there */
|
||||
msg("");
|
||||
}
|
||||
}
|
||||
else
|
||||
ch = '.';
|
||||
if (no_command)
|
||||
{
|
||||
if (--no_command == 0)
|
||||
{
|
||||
player.t_flags |= ISRUN;
|
||||
msg("you can move again");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
* check for prefixes
|
||||
*/
|
||||
newcount = FALSE;
|
||||
if (isdigit(ch))
|
||||
{
|
||||
count = 0;
|
||||
newcount = TRUE;
|
||||
while (isdigit(ch))
|
||||
{
|
||||
count = count * 10 + (ch - '0');
|
||||
if (count > 255)
|
||||
count = 255;
|
||||
ch = readchar();
|
||||
}
|
||||
countch = ch;
|
||||
/*
|
||||
* turn off count for commands which don't make sense
|
||||
* to repeat
|
||||
*/
|
||||
switch (ch)
|
||||
{
|
||||
case CTRL('B'): case CTRL('H'): case CTRL('J'):
|
||||
case CTRL('K'): case CTRL('L'): case CTRL('N'):
|
||||
case CTRL('U'): case CTRL('Y'):
|
||||
case '.': case 'a': case 'b': case 'h': case 'j':
|
||||
case 'k': case 'l': case 'm': case 'n': case 'q':
|
||||
case 'r': case 's': case 't': case 'u': case 'y':
|
||||
case 'z': case 'B': case 'C': case 'H': case 'I':
|
||||
case 'J': case 'K': case 'L': case 'N': case 'U':
|
||||
case 'Y':
|
||||
#ifdef MASTER
|
||||
case CTRL('D'): case CTRL('A'):
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* execute a command
|
||||
*/
|
||||
if (count && !running)
|
||||
count--;
|
||||
if (ch != 'a' && ch != ESCAPE && !(running || count || to_death))
|
||||
{
|
||||
l_last_comm = last_comm;
|
||||
l_last_dir = last_dir;
|
||||
l_last_pick = last_pick;
|
||||
last_comm = ch;
|
||||
last_dir = '\0';
|
||||
last_pick = NULL;
|
||||
}
|
||||
over:
|
||||
switch (ch)
|
||||
{
|
||||
case ',': {
|
||||
THING *obj = NULL;
|
||||
int found = 0;
|
||||
for (obj = lvl_obj; obj != NULL; obj = next(obj))
|
||||
{
|
||||
if (obj->o_pos.y == hero.y && obj->o_pos.x == hero.x)
|
||||
{
|
||||
found=1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
if (levit_check())
|
||||
;
|
||||
else
|
||||
pick_up((char)obj->o_type);
|
||||
}
|
||||
else {
|
||||
if (!terse)
|
||||
addmsg("there is ");
|
||||
addmsg("nothing here");
|
||||
if (!terse)
|
||||
addmsg(" to pick up");
|
||||
endmsg();
|
||||
}
|
||||
}
|
||||
when '!': shell();
|
||||
when 'h': do_move(0, -1);
|
||||
when 'j': do_move(1, 0);
|
||||
when 'k': do_move(-1, 0);
|
||||
when 'l': do_move(0, 1);
|
||||
when 'y': do_move(-1, -1);
|
||||
when 'u': do_move(-1, 1);
|
||||
when 'b': do_move(1, -1);
|
||||
when 'n': do_move(1, 1);
|
||||
when 'H': do_run('h');
|
||||
when 'J': do_run('j');
|
||||
when 'K': do_run('k');
|
||||
when 'L': do_run('l');
|
||||
when 'Y': do_run('y');
|
||||
when 'U': do_run('u');
|
||||
when 'B': do_run('b');
|
||||
when 'N': do_run('n');
|
||||
when CTRL('H'): case CTRL('J'): case CTRL('K'): case CTRL('L'):
|
||||
case CTRL('Y'): case CTRL('U'): case CTRL('B'): case CTRL('N'):
|
||||
{
|
||||
if (!on(player, ISBLIND))
|
||||
{
|
||||
door_stop = TRUE;
|
||||
firstmove = TRUE;
|
||||
}
|
||||
if (count && !newcount)
|
||||
ch = direction;
|
||||
else
|
||||
{
|
||||
ch += ('A' - CTRL('A'));
|
||||
direction = ch;
|
||||
}
|
||||
goto over;
|
||||
}
|
||||
when 'F':
|
||||
kamikaze = TRUE;
|
||||
/* FALLTHROUGH */
|
||||
case 'f':
|
||||
if (!get_dir())
|
||||
{
|
||||
after = FALSE;
|
||||
break;
|
||||
}
|
||||
delta.y += hero.y;
|
||||
delta.x += hero.x;
|
||||
if ( ((mp = moat(delta.y, delta.x)) == NULL)
|
||||
|| ((!see_monst(mp)) && !on(player, SEEMONST)))
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("I see ");
|
||||
msg("no monster there");
|
||||
after = FALSE;
|
||||
}
|
||||
else if (diag_ok(&hero, &delta))
|
||||
{
|
||||
to_death = TRUE;
|
||||
max_hit = 0;
|
||||
mp->t_flags |= ISTARGET;
|
||||
runch = ch = dir_ch;
|
||||
goto over;
|
||||
}
|
||||
when 't':
|
||||
if (!get_dir())
|
||||
after = FALSE;
|
||||
else
|
||||
missile(delta.y, delta.x);
|
||||
when 'a':
|
||||
if (last_comm == '\0')
|
||||
{
|
||||
msg("you haven't typed a command yet");
|
||||
after = FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
ch = last_comm;
|
||||
again = TRUE;
|
||||
goto over;
|
||||
}
|
||||
when 'q': quaff();
|
||||
when 'Q':
|
||||
after = FALSE;
|
||||
q_comm = TRUE;
|
||||
quit(0);
|
||||
q_comm = FALSE;
|
||||
when 'i': after = FALSE; inventory(pack, 0);
|
||||
when 'I': after = FALSE; picky_inven();
|
||||
when 'd': drop();
|
||||
when 'r': read_scroll();
|
||||
when 'e': eat();
|
||||
when 'w': wield();
|
||||
when 'W': wear();
|
||||
when 'T': take_off();
|
||||
when 'P': ring_on();
|
||||
when 'R': ring_off();
|
||||
when 'o': option(); after = FALSE;
|
||||
when 'c': call(); after = FALSE;
|
||||
when '>': after = FALSE; d_level();
|
||||
when '<': after = FALSE; u_level();
|
||||
when '?': after = FALSE; help();
|
||||
when '/': after = FALSE; identify();
|
||||
when 's': search();
|
||||
when 'z':
|
||||
if (get_dir())
|
||||
do_zap();
|
||||
else
|
||||
after = FALSE;
|
||||
when 'D': after = FALSE; discovered();
|
||||
when CTRL('P'): after = FALSE; msg(huh);
|
||||
when CTRL('R'):
|
||||
after = FALSE;
|
||||
clearok(curscr,TRUE);
|
||||
wrefresh(curscr);
|
||||
when 'v':
|
||||
after = FALSE;
|
||||
msg("version %s. (mctesq was here)", release);
|
||||
when 'S':
|
||||
after = FALSE;
|
||||
save_game();
|
||||
when '.': ; /* Rest command */
|
||||
when ' ': after = FALSE; /* "Legal" illegal command */
|
||||
when '^':
|
||||
after = FALSE;
|
||||
if (get_dir()) {
|
||||
delta.y += hero.y;
|
||||
delta.x += hero.x;
|
||||
fp = &flat(delta.y, delta.x);
|
||||
if (!terse)
|
||||
addmsg("You have found ");
|
||||
if (chat(delta.y, delta.x) != TRAP)
|
||||
msg("no trap there");
|
||||
else if (on(player, ISHALU))
|
||||
msg(tr_name[rnd(NTRAPS)]);
|
||||
else {
|
||||
msg(tr_name[*fp & F_TMASK]);
|
||||
*fp |= F_SEEN;
|
||||
}
|
||||
}
|
||||
#ifdef MASTER
|
||||
when '+':
|
||||
after = FALSE;
|
||||
if (wizard)
|
||||
{
|
||||
wizard = FALSE;
|
||||
turn_see(TRUE);
|
||||
msg("not wizard any more");
|
||||
}
|
||||
else
|
||||
{
|
||||
wizard = passwd();
|
||||
if (wizard)
|
||||
{
|
||||
noscore = TRUE;
|
||||
turn_see(FALSE);
|
||||
msg("you are suddenly as smart as Ken Arnold in dungeon #%d", dnum);
|
||||
}
|
||||
else
|
||||
msg("sorry");
|
||||
}
|
||||
#endif
|
||||
when ESCAPE: /* Escape */
|
||||
door_stop = FALSE;
|
||||
count = 0;
|
||||
after = FALSE;
|
||||
again = FALSE;
|
||||
when 'm':
|
||||
move_on = TRUE;
|
||||
if (!get_dir())
|
||||
after = FALSE;
|
||||
else
|
||||
{
|
||||
ch = dir_ch;
|
||||
countch = dir_ch;
|
||||
goto over;
|
||||
}
|
||||
when ')': current(cur_weapon, "wielding", NULL);
|
||||
when ']': current(cur_armor, "wearing", NULL);
|
||||
when '=':
|
||||
current(cur_ring[LEFT], "wearing",
|
||||
terse ? "(L)" : "on left hand");
|
||||
current(cur_ring[RIGHT], "wearing",
|
||||
terse ? "(R)" : "on right hand");
|
||||
when '@':
|
||||
stat_msg = TRUE;
|
||||
status();
|
||||
stat_msg = FALSE;
|
||||
after = FALSE;
|
||||
otherwise:
|
||||
after = FALSE;
|
||||
#ifdef MASTER
|
||||
if (wizard) switch (ch)
|
||||
{
|
||||
case '|': msg("@ %d,%d", hero.y, hero.x);
|
||||
when 'C': create_obj();
|
||||
when '$': msg("inpack = %d", inpack);
|
||||
when CTRL('G'): inventory(lvl_obj, 0);
|
||||
when CTRL('W'): whatis(FALSE, 0);
|
||||
when CTRL('D'): level++; new_level();
|
||||
when CTRL('A'): level--; new_level();
|
||||
when CTRL('F'): show_map();
|
||||
when CTRL('T'): teleport();
|
||||
when CTRL('E'): msg("food left: %d", food_left);
|
||||
when CTRL('C'): add_pass();
|
||||
when CTRL('X'): turn_see(on(player, SEEMONST));
|
||||
when CTRL('~'):
|
||||
{
|
||||
THING *item;
|
||||
|
||||
if ((item = get_item("charge", STICK)) != NULL)
|
||||
item->o_charges = 10000;
|
||||
}
|
||||
when CTRL('I'):
|
||||
{
|
||||
int i;
|
||||
THING *obj;
|
||||
|
||||
for (i = 0; i < 9; i++)
|
||||
raise_level();
|
||||
/*
|
||||
* Give him a sword (+1,+1)
|
||||
*/
|
||||
obj = new_item();
|
||||
init_weapon(obj, TWOSWORD);
|
||||
obj->o_hplus = 1;
|
||||
obj->o_dplus = 1;
|
||||
add_pack(obj, TRUE);
|
||||
cur_weapon = obj;
|
||||
/*
|
||||
* And his suit of armor
|
||||
*/
|
||||
obj = new_item();
|
||||
obj->o_type = ARMOR;
|
||||
obj->o_which = PLATE_MAIL;
|
||||
obj->o_arm = -5;
|
||||
obj->o_flags |= ISKNOW;
|
||||
obj->o_count = 1;
|
||||
obj->o_group = 0;
|
||||
cur_armor = obj;
|
||||
add_pack(obj, TRUE);
|
||||
}
|
||||
when '*' :
|
||||
pr_list();
|
||||
otherwise:
|
||||
illcom(ch);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
illcom(ch);
|
||||
}
|
||||
/*
|
||||
* turn off flags if no longer needed
|
||||
*/
|
||||
if (!running)
|
||||
door_stop = FALSE;
|
||||
}
|
||||
/*
|
||||
* If he ran into something to take, let him pick it up.
|
||||
*/
|
||||
if (take != 0)
|
||||
pick_up(take);
|
||||
if (!running)
|
||||
door_stop = FALSE;
|
||||
if (!after)
|
||||
ntimes++;
|
||||
}
|
||||
do_daemons(AFTER);
|
||||
do_fuses(AFTER);
|
||||
if (ISRING(LEFT, R_SEARCH))
|
||||
search();
|
||||
else if (ISRING(LEFT, R_TELEPORT) && rnd(50) == 0)
|
||||
teleport();
|
||||
if (ISRING(RIGHT, R_SEARCH))
|
||||
search();
|
||||
else if (ISRING(RIGHT, R_TELEPORT) && rnd(50) == 0)
|
||||
teleport();
|
||||
}
|
||||
|
||||
/*
|
||||
* illcom:
|
||||
* What to do with an illegal command
|
||||
*/
|
||||
void
|
||||
illcom(int ch)
|
||||
{
|
||||
save_msg = FALSE;
|
||||
count = 0;
|
||||
msg("illegal command '%s'", unctrl(ch));
|
||||
save_msg = TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* search:
|
||||
* player gropes about him to find hidden things.
|
||||
*/
|
||||
void
|
||||
search()
|
||||
{
|
||||
register int y, x;
|
||||
register char *fp;
|
||||
register int ey, ex;
|
||||
int probinc;
|
||||
bool found;
|
||||
|
||||
ey = hero.y + 1;
|
||||
ex = hero.x + 1;
|
||||
probinc = (on(player, ISHALU) ? 3 : 0);
|
||||
probinc += (on(player, ISBLIND) ? 2 : 0);
|
||||
found = FALSE;
|
||||
for (y = hero.y - 1; y <= ey; y++)
|
||||
for (x = hero.x - 1; x <= ex; x++)
|
||||
{
|
||||
if (y == hero.y && x == hero.x)
|
||||
continue;
|
||||
fp = &flat(y, x);
|
||||
if (!(*fp & F_REAL))
|
||||
switch (chat(y, x))
|
||||
{
|
||||
case '|':
|
||||
case '-':
|
||||
if (rnd(5 + probinc) != 0)
|
||||
break;
|
||||
chat(y, x) = DOOR;
|
||||
msg("a secret door");
|
||||
foundone:
|
||||
found = TRUE;
|
||||
*fp |= F_REAL;
|
||||
count = FALSE;
|
||||
running = FALSE;
|
||||
break;
|
||||
case FLOOR:
|
||||
if (rnd(2 + probinc) != 0)
|
||||
break;
|
||||
chat(y, x) = TRAP;
|
||||
if (!terse)
|
||||
addmsg("you found ");
|
||||
if (on(player, ISHALU))
|
||||
msg(tr_name[rnd(NTRAPS)]);
|
||||
else {
|
||||
msg(tr_name[*fp & F_TMASK]);
|
||||
*fp |= F_SEEN;
|
||||
}
|
||||
goto foundone;
|
||||
break;
|
||||
case ' ':
|
||||
if (rnd(3 + probinc) != 0)
|
||||
break;
|
||||
chat(y, x) = PASSAGE;
|
||||
goto foundone;
|
||||
}
|
||||
}
|
||||
if (found)
|
||||
look(FALSE);
|
||||
}
|
||||
|
||||
/*
|
||||
* help:
|
||||
* Give single character help, or the whole mess if he wants it
|
||||
*/
|
||||
void
|
||||
help()
|
||||
{
|
||||
register struct h_list *strp;
|
||||
register char helpch;
|
||||
register int numprint, cnt;
|
||||
msg("character you want help for (* for all): ");
|
||||
helpch = readchar();
|
||||
mpos = 0;
|
||||
/*
|
||||
* If its not a *, print the right help string
|
||||
* or an error if he typed a funny character.
|
||||
*/
|
||||
if (helpch != '*')
|
||||
{
|
||||
move(0, 0);
|
||||
for (strp = helpstr; strp->h_desc != NULL; strp++)
|
||||
if (strp->h_ch == helpch)
|
||||
{
|
||||
lower_msg = TRUE;
|
||||
msg("%s%s", unctrl(strp->h_ch), strp->h_desc);
|
||||
lower_msg = FALSE;
|
||||
return;
|
||||
}
|
||||
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 = helpstr; strp->h_desc != NULL; strp++)
|
||||
if (strp->h_print)
|
||||
numprint++;
|
||||
if (numprint & 01) /* round odd numbers up */
|
||||
numprint++;
|
||||
numprint /= 2;
|
||||
if (numprint > LINES - 1)
|
||||
numprint = LINES - 1;
|
||||
|
||||
wclear(hw);
|
||||
cnt = 0;
|
||||
for (strp = helpstr; strp->h_desc != NULL; strp++)
|
||||
if (strp->h_print)
|
||||
{
|
||||
wmove(hw, cnt % numprint, cnt >= numprint ? COLS / 2 : 0);
|
||||
if (strp->h_ch)
|
||||
waddstr(hw, unctrl(strp->h_ch));
|
||||
waddstr(hw, strp->h_desc);
|
||||
if (++cnt >= numprint * 2)
|
||||
break;
|
||||
}
|
||||
wmove(hw, LINES - 1, 0);
|
||||
waddstr(hw, "--Press space to continue--");
|
||||
wrefresh(hw);
|
||||
wait_for(' ');
|
||||
clearok(stdscr, TRUE);
|
||||
/*
|
||||
refresh();
|
||||
*/
|
||||
msg("");
|
||||
touchwin(stdscr);
|
||||
wrefresh(stdscr);
|
||||
}
|
||||
|
||||
/*
|
||||
* identify:
|
||||
* Tell the player what a certain thing is.
|
||||
*/
|
||||
void
|
||||
identify()
|
||||
{
|
||||
register int ch;
|
||||
register struct h_list *hp;
|
||||
register char *str;
|
||||
static struct h_list ident_list[] = {
|
||||
{'|', "wall of a room", FALSE},
|
||||
{'-', "wall of a room", FALSE},
|
||||
{GOLD, "gold", FALSE},
|
||||
{STAIRS, "a staircase", FALSE},
|
||||
{DOOR, "door", FALSE},
|
||||
{FLOOR, "room floor", FALSE},
|
||||
{PLAYER, "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},
|
||||
{'\0'}
|
||||
};
|
||||
|
||||
msg("what do you want identified? ");
|
||||
ch = readchar();
|
||||
mpos = 0;
|
||||
if (ch == ESCAPE)
|
||||
{
|
||||
msg("");
|
||||
return;
|
||||
}
|
||||
if (isupper(ch))
|
||||
str = monsters[ch-'A'].m_name;
|
||||
else
|
||||
{
|
||||
str = "unknown character";
|
||||
for (hp = ident_list; hp->h_ch != '\0'; hp++)
|
||||
if (hp->h_ch == ch)
|
||||
{
|
||||
str = hp->h_desc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
msg("'%s': %s", unctrl(ch), str);
|
||||
}
|
||||
|
||||
/*
|
||||
* d_level:
|
||||
* He wants to go down a level
|
||||
*/
|
||||
void
|
||||
d_level()
|
||||
{
|
||||
if (levit_check())
|
||||
return;
|
||||
if (chat(hero.y, hero.x) != STAIRS)
|
||||
msg("I see no way down");
|
||||
else
|
||||
{
|
||||
level++;
|
||||
seenstairs = FALSE;
|
||||
new_level();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* u_level:
|
||||
* He wants to go up a level
|
||||
*/
|
||||
void
|
||||
u_level()
|
||||
{
|
||||
if (levit_check())
|
||||
return;
|
||||
if (chat(hero.y, hero.x) == STAIRS)
|
||||
if (amulet)
|
||||
{
|
||||
level--;
|
||||
if (level == 0)
|
||||
total_winner();
|
||||
new_level();
|
||||
msg("you feel a wrenching sensation in your gut");
|
||||
}
|
||||
else
|
||||
msg("your way is magically blocked");
|
||||
else
|
||||
msg("I see no way up");
|
||||
}
|
||||
|
||||
/*
|
||||
* levit_check:
|
||||
* Check to see if she's levitating, and if she is, print an
|
||||
* appropriate message.
|
||||
*/
|
||||
bool
|
||||
levit_check()
|
||||
{
|
||||
if (!on(player, ISLEVIT))
|
||||
return FALSE;
|
||||
msg("You can't. You're floating off the ground!");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* call:
|
||||
* Allow a user to call a potion, scroll, or ring something
|
||||
*/
|
||||
void
|
||||
call()
|
||||
{
|
||||
register THING *obj;
|
||||
register struct obj_info *op = NULL;
|
||||
register char **guess, *elsewise = NULL;
|
||||
register bool *know;
|
||||
|
||||
obj = get_item("call", CALLABLE);
|
||||
/*
|
||||
* Make certain that it is somethings that we want to wear
|
||||
*/
|
||||
if (obj == NULL)
|
||||
return;
|
||||
switch (obj->o_type)
|
||||
{
|
||||
case RING:
|
||||
op = &ring_info[obj->o_which];
|
||||
elsewise = r_stones[obj->o_which];
|
||||
goto norm;
|
||||
when POTION:
|
||||
op = &pot_info[obj->o_which];
|
||||
elsewise = p_colors[obj->o_which];
|
||||
goto norm;
|
||||
when SCROLL:
|
||||
op = &scr_info[obj->o_which];
|
||||
elsewise = s_names[obj->o_which];
|
||||
goto norm;
|
||||
when STICK:
|
||||
op = &ws_info[obj->o_which];
|
||||
elsewise = ws_made[obj->o_which];
|
||||
norm:
|
||||
know = &op->oi_know;
|
||||
guess = &op->oi_guess;
|
||||
if (*guess != NULL)
|
||||
elsewise = *guess;
|
||||
when FOOD:
|
||||
msg("you can't call that anything");
|
||||
return;
|
||||
otherwise:
|
||||
guess = &obj->o_label;
|
||||
know = NULL;
|
||||
elsewise = obj->o_label;
|
||||
}
|
||||
if (know != NULL && *know)
|
||||
{
|
||||
msg("that has already been identified");
|
||||
return;
|
||||
}
|
||||
if (elsewise != NULL && elsewise == *guess)
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("Was ");
|
||||
msg("called \"%s\"", elsewise);
|
||||
}
|
||||
if (terse)
|
||||
msg("call it: ");
|
||||
else
|
||||
msg("what do you want to call it? ");
|
||||
|
||||
if (elsewise == NULL)
|
||||
strcpy(prbuf, "");
|
||||
else
|
||||
strcpy(prbuf, elsewise);
|
||||
if (get_str(prbuf, stdscr) == NORM)
|
||||
{
|
||||
if (*guess != NULL)
|
||||
free(*guess);
|
||||
*guess = malloc((unsigned int) strlen(prbuf) + 1);
|
||||
strcpy(*guess, prbuf);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* current:
|
||||
* Print the current weapon/armor
|
||||
*/
|
||||
void
|
||||
current(THING *cur, char *how, char *where)
|
||||
{
|
||||
after = FALSE;
|
||||
if (cur != NULL)
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("you are %s (", how);
|
||||
inv_describe = FALSE;
|
||||
addmsg("%c) %s", cur->o_packch, inv_name(cur, TRUE));
|
||||
inv_describe = TRUE;
|
||||
if (where)
|
||||
addmsg(" %s", where);
|
||||
endmsg();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("you are ");
|
||||
addmsg("%s nothing", how);
|
||||
if (where)
|
||||
addmsg(" %s", where);
|
||||
endmsg();
|
||||
}
|
||||
}
|
||||
1500
config.guess
vendored
Executable file
1500
config.guess
vendored
Executable file
File diff suppressed because it is too large
Load Diff
269
config.h.in
Normal file
269
config.h.in
Normal file
@@ -0,0 +1,269 @@
|
||||
/* config.h.in. Generated from configure.ac by autoheader. */
|
||||
|
||||
/* Define if scorefile is top scores, not top players */
|
||||
#undef ALLSCORES
|
||||
|
||||
/* Define if checktime feature should be enabled */
|
||||
#undef CHECKTIME
|
||||
|
||||
/* Define to group owner of setgid executable */
|
||||
#undef GROUPOWNER
|
||||
|
||||
/* Define to 1 if you have the `alarm' function. */
|
||||
#undef HAVE_ALARM
|
||||
|
||||
/* Define to 1 if you have the <arpa/inet.h> header file. */
|
||||
#undef HAVE_ARPA_INET_H
|
||||
|
||||
/* Define to 1 if libcurses is requested */
|
||||
#undef HAVE_CURSES_H
|
||||
|
||||
/* Define to 1 if you don't have `vprintf' but do have `_doprnt.' */
|
||||
#undef HAVE_DOPRNT
|
||||
|
||||
/* Define to 1 if you have the `erasechar' function. */
|
||||
#undef HAVE_ERASECHAR
|
||||
|
||||
/* Define if ncurses has ESCDELAY variable */
|
||||
#undef HAVE_ESCDELAY
|
||||
|
||||
/* Define to 1 if you have the <fcntl.h> header file. */
|
||||
#undef HAVE_FCNTL_H
|
||||
|
||||
/* Define to 1 if you have the `fork' function. */
|
||||
#undef HAVE_FORK
|
||||
|
||||
/* Define to 1 if you have the `getgid' function. */
|
||||
#undef HAVE_GETGID
|
||||
|
||||
/* Define to 1 if you have the `getloadavg' function. */
|
||||
#undef HAVE_GETLOADAVG
|
||||
|
||||
/* Define to 1 if you have the `getpass' function. */
|
||||
#undef HAVE_GETPASS
|
||||
|
||||
/* Define to 1 if you have the `getpwuid' function. */
|
||||
#undef HAVE_GETPWUID
|
||||
|
||||
/* Define to 1 if you have the `getuid' function. */
|
||||
#undef HAVE_GETUID
|
||||
|
||||
/* Define to 1 if you have the <inttypes.h> header file. */
|
||||
#undef HAVE_INTTYPES_H
|
||||
|
||||
/* Define to 1 if you have the `killchar' function. */
|
||||
#undef HAVE_KILLCHAR
|
||||
|
||||
/* Define to 1 if you have the <limits.h> header file. */
|
||||
#undef HAVE_LIMITS_H
|
||||
|
||||
/* Define to 1 if you have the `loadav' function. */
|
||||
#undef HAVE_LOADAV
|
||||
|
||||
/* Define to 1 if `lstat' has the bug that it succeeds when given the
|
||||
zero-length file name argument. */
|
||||
#undef HAVE_LSTAT_EMPTY_STRING_BUG
|
||||
|
||||
/* Define to 1 if you have the <memory.h> header file. */
|
||||
#undef HAVE_MEMORY_H
|
||||
|
||||
/* Define to 1 if you have the `memset' function. */
|
||||
#undef HAVE_MEMSET
|
||||
|
||||
/* Define to 1 if libncurses is requested */
|
||||
#undef HAVE_NCURSES_H
|
||||
|
||||
/* Define to 1 if you have the <ncurses/term.h> header file. */
|
||||
#undef HAVE_NCURSES_TERM_H
|
||||
|
||||
/* Define to 1 if you have the `nlist' function. */
|
||||
#undef HAVE_NLIST
|
||||
|
||||
/* Define to 1 if you have the <nlist.h> header file. */
|
||||
#undef HAVE_NLIST_H
|
||||
|
||||
/* Define to 1 if you have the <process.h> header file. */
|
||||
#undef HAVE_PROCESS_H
|
||||
|
||||
/* Define to 1 if you have the <pwd.h> header file. */
|
||||
#undef HAVE_PWD_H
|
||||
|
||||
/* Define to 1 if you have the `setenv' function. */
|
||||
#undef HAVE_SETENV
|
||||
|
||||
/* Define to 1 if you have the `setgid' function. */
|
||||
#undef HAVE_SETGID
|
||||
|
||||
/* Define to 1 if you have the `setregid' function. */
|
||||
#undef HAVE_SETREGID
|
||||
|
||||
/* Define to 1 if you have the `setresgid' function. */
|
||||
#undef HAVE_SETRESGID
|
||||
|
||||
/* Define to 1 if you have the `setresuid' function. */
|
||||
#undef HAVE_SETRESUID
|
||||
|
||||
/* Define to 1 if you have the `setreuid' function. */
|
||||
#undef HAVE_SETREUID
|
||||
|
||||
/* Define to 1 if you have the `setuid' function. */
|
||||
#undef HAVE_SETUID
|
||||
|
||||
/* Define to 1 if you have the `spawnl' function. */
|
||||
#undef HAVE_SPAWNL
|
||||
|
||||
/* Define to 1 if `stat' has the bug that it succeeds when given the
|
||||
zero-length file name argument. */
|
||||
#undef HAVE_STAT_EMPTY_STRING_BUG
|
||||
|
||||
/* Define to 1 if stdbool.h conforms to C99. */
|
||||
#undef HAVE_STDBOOL_H
|
||||
|
||||
/* Define to 1 if you have the <stdint.h> header file. */
|
||||
#undef HAVE_STDINT_H
|
||||
|
||||
/* Define to 1 if you have the <stdlib.h> header file. */
|
||||
#undef HAVE_STDLIB_H
|
||||
|
||||
/* Define to 1 if you have the `strchr' function. */
|
||||
#undef HAVE_STRCHR
|
||||
|
||||
/* Define to 1 if you have the `strerror' function. */
|
||||
#undef HAVE_STRERROR
|
||||
|
||||
/* Define to 1 if you have the <strings.h> header file. */
|
||||
#undef HAVE_STRINGS_H
|
||||
|
||||
/* Define to 1 if you have the <string.h> header file. */
|
||||
#undef HAVE_STRING_H
|
||||
|
||||
/* Define to 1 if you have the <sys/ioctl.h> header file. */
|
||||
#undef HAVE_SYS_IOCTL_H
|
||||
|
||||
/* Define to 1 if you have the <sys/stat.h> header file. */
|
||||
#undef HAVE_SYS_STAT_H
|
||||
|
||||
/* Define to 1 if you have the <sys/types.h> header file. */
|
||||
#undef HAVE_SYS_TYPES_H
|
||||
|
||||
/* Define to 1 if you have the <sys/utsname.h> header file. */
|
||||
#undef HAVE_SYS_UTSNAME_H
|
||||
|
||||
/* Define to 1 if you have the <termios.h> header file. */
|
||||
#undef HAVE_TERMIOS_H
|
||||
|
||||
/* Define to 1 if you have the <term.h> header file. */
|
||||
#undef HAVE_TERM_H
|
||||
|
||||
/* Define to 1 if you have the <unistd.h> header file. */
|
||||
#undef HAVE_UNISTD_H
|
||||
|
||||
/* Define to 1 if you have the <utmp.h> header file. */
|
||||
#undef HAVE_UTMP_H
|
||||
|
||||
/* Define to 1 if you have the `vfork' function. */
|
||||
#undef HAVE_VFORK
|
||||
|
||||
/* Define to 1 if you have the <vfork.h> header file. */
|
||||
#undef HAVE_VFORK_H
|
||||
|
||||
/* Define to 1 if you have the `vprintf' function. */
|
||||
#undef HAVE_VPRINTF
|
||||
|
||||
/* Define to 1 if `fork' works. */
|
||||
#undef HAVE_WORKING_FORK
|
||||
|
||||
/* Define to 1 if `vfork' works. */
|
||||
#undef HAVE_WORKING_VFORK
|
||||
|
||||
/* Define to 1 if the system has the type `_Bool'. */
|
||||
#undef HAVE__BOOL
|
||||
|
||||
/* Define to 1 if you have the `_spawnl' function. */
|
||||
#undef HAVE__SPAWNL
|
||||
|
||||
/* define if we should use program's load average function instead of system
|
||||
*/
|
||||
#undef LOADAV
|
||||
|
||||
/* Define to file to use for scoreboard lockfile */
|
||||
#undef LOCKFILE
|
||||
|
||||
/* Define to 1 if `lstat' dereferences a symlink specified with a trailing
|
||||
slash. */
|
||||
#undef LSTAT_FOLLOWS_SLASHED_SYMLINK
|
||||
|
||||
/* Define to include wizard mode */
|
||||
#undef MASTER
|
||||
|
||||
/* Define if maxusers feature should be enabled */
|
||||
#undef MAXLOAD
|
||||
|
||||
/* Define if maxusers feature should be enabled */
|
||||
#undef MAXUSERS
|
||||
|
||||
/* kernel file to pass to nlist() when reading load average (unlikely to work)
|
||||
*/
|
||||
#undef NAMELIST
|
||||
|
||||
/* word for the number of scores to store in scoreboard */
|
||||
#undef NUMNAME
|
||||
|
||||
/* number of scores to store in scoreboard */
|
||||
#undef NUMSCORES
|
||||
|
||||
/* Define to the address where bug reports for this package should be sent. */
|
||||
#undef PACKAGE_BUGREPORT
|
||||
|
||||
/* Define to the full name of this package. */
|
||||
#undef PACKAGE_NAME
|
||||
|
||||
/* Define to the full name and version of this package. */
|
||||
#undef PACKAGE_STRING
|
||||
|
||||
/* Define to the one symbol short name of this package. */
|
||||
#undef PACKAGE_TARNAME
|
||||
|
||||
/* Define to the version of this package. */
|
||||
#undef PACKAGE_VERSION
|
||||
|
||||
/* Define crypt(3) wizard mode password */
|
||||
#undef PASSWD
|
||||
|
||||
/* Define as the return type of signal handlers (`int' or `void'). */
|
||||
#undef RETSIGTYPE
|
||||
|
||||
/* Define to file to use for scoreboard */
|
||||
#undef SCOREFILE
|
||||
|
||||
/* Define to 1 if you have the ANSI C header files. */
|
||||
#undef STDC_HEADERS
|
||||
|
||||
/* Define to 1 if your <sys/time.h> declares `struct tm'. */
|
||||
#undef TM_IN_SYS_TIME
|
||||
|
||||
/* define if we should use program's user counting function instead of
|
||||
system's */
|
||||
#undef UCOUNT
|
||||
|
||||
/* utmp like file to pass to ucount() when counting online users (unlikely to
|
||||
work) */
|
||||
#undef UTMP
|
||||
|
||||
/* Define to empty if `const' does not conform to ANSI C. */
|
||||
#undef const
|
||||
|
||||
/* Define to `int' if <sys/types.h> doesn't define. */
|
||||
#undef gid_t
|
||||
|
||||
/* Define to `int' if <sys/types.h> does not define. */
|
||||
#undef pid_t
|
||||
|
||||
/* Define to `unsigned int' if <sys/types.h> does not define. */
|
||||
#undef size_t
|
||||
|
||||
/* Define to `int' if <sys/types.h> doesn't define. */
|
||||
#undef uid_t
|
||||
|
||||
/* Define as `fork' if `vfork' does not work. */
|
||||
#undef vfork
|
||||
1608
config.sub
vendored
Executable file
1608
config.sub
vendored
Executable file
File diff suppressed because it is too large
Load Diff
246
configure.ac
Normal file
246
configure.ac
Normal file
@@ -0,0 +1,246 @@
|
||||
# -*- Autoconf -*-
|
||||
# Process this file with autoconf to produce a configure script.
|
||||
|
||||
AC_PREREQ(2.56)
|
||||
AC_INIT([Rogue],[5.4.4], [yendor@rogueforge.net])
|
||||
AC_CONFIG_SRCDIR([armor.c])
|
||||
AC_CONFIG_HEADER([config.h])
|
||||
AC_CONFIG_FILES([Makefile rogue.6 rogue.cat rogue.doc rogue.html rogue.me])
|
||||
AC_CANONICAL_SYSTEM([])
|
||||
|
||||
# Checks for programs.
|
||||
AC_PROG_CC
|
||||
|
||||
# Checks for libraries.
|
||||
|
||||
# Checks for header files.
|
||||
AC_HEADER_STDC
|
||||
AC_CHECK_HEADERS([arpa/inet.h sys/utsname.h pwd.h fcntl.h limits.h nlist.h stdlib.h string.h sys/ioctl.h termios.h unistd.h utmp.h term.h ncurses/term.h process.h])
|
||||
|
||||
# Checks for typedefs, structures, and compiler characteristics.
|
||||
AC_HEADER_STDBOOL
|
||||
AC_C_CONST
|
||||
AC_TYPE_UID_T
|
||||
AC_TYPE_SIZE_T
|
||||
AC_STRUCT_TM
|
||||
MP_WITH_CURSES
|
||||
# Checks for library functions.
|
||||
AC_FUNC_FORK
|
||||
AC_PROG_GCC_TRADITIONAL
|
||||
AC_FUNC_LSTAT
|
||||
AC_FUNC_LSTAT_FOLLOWS_SLASHED_SYMLINK
|
||||
AC_TYPE_SIGNAL
|
||||
AC_FUNC_STAT
|
||||
AC_FUNC_VPRINTF
|
||||
AC_CHECK_FUNCS([erasechar killchar alarm getpass memset setenv strchr nlist _spawnl spawnl getpwuid loadav getloadavg strerror setresgid setregid setgid setresuid setreuid setuid getuid getgid])
|
||||
|
||||
AC_CHECK_PROG([NROFF], [nroff], [nroff],)
|
||||
AC_CHECK_PROG([GROFF], [groff], [groff],)
|
||||
AC_CHECK_PROG([COLCRT], [colcrt], [colcrt],)
|
||||
AC_CHECK_PROG([TBL], [tbl], [tbl],)
|
||||
AC_CHECK_PROG([SED], [sed], [sed],)
|
||||
|
||||
AC_ARG_WITH(program-name, AC_HELP_STRING([--with-program-name=NAME],[alternate executable name]),[progname="$withval" ], [progname="rogue"] )
|
||||
PROGRAM=$progname
|
||||
AC_SUBST(PROGRAM)
|
||||
|
||||
AC_ARG_ENABLE(setgid, AC_HELP_STRING([--enable-setgid=NAME],[install executable as setgid with group ownership of NAME @<:@default=no@:>@])],[],[])
|
||||
AC_MSG_CHECKING([if using setgid execute bit])
|
||||
if test "x$enable_setgid" = "xno" ; then
|
||||
GROUPOWNER=
|
||||
elif test "x$enable_setgid" = "xyes" ; then
|
||||
GROUPOWNER=games
|
||||
elif test "x$enable_setgid" = "x" ; then
|
||||
GROUPOWNER=
|
||||
else
|
||||
GROUPOWNER=$enable_setgid
|
||||
fi
|
||||
|
||||
if test "x$GROUPOWNER" != "x" ; then
|
||||
AC_DEFINE_UNQUOTED([GROUPOWNER],[$GROUPOWNER], [Define to group owner of setgid executable])
|
||||
AC_MSG_RESULT([$GROUPOWNER])
|
||||
else
|
||||
AC_MSG_RESULT([no])
|
||||
fi
|
||||
|
||||
AC_SUBST(GROUPOWNER)
|
||||
|
||||
AC_ARG_ENABLE([scorefile],[AC_HELP_STRING([--enable-scorefile=SCOREFILE], [enable scoreboard with given filename])],[],[])
|
||||
AC_MSG_CHECKING([for scoreboard file])
|
||||
if test "x$enable_scorefile" = "xno" ; then
|
||||
SCOREFILE=
|
||||
elif test "x$enable_scorefile" = "xyes" ; then
|
||||
SCOREFILE=$progname.scr
|
||||
elif test "x$enable_scorefile" = "x" ; then
|
||||
SCOREFILE=$progname.scr
|
||||
else
|
||||
SCOREFILE=$enable_scorefile
|
||||
fi
|
||||
|
||||
if test "x$SCOREFILE" != "x" ; then
|
||||
AC_DEFINE_UNQUOTED([SCOREFILE], ["$SCOREFILE"], [Define to file to use for scoreboard])
|
||||
AC_MSG_RESULT([$SCOREFILE])
|
||||
else
|
||||
AC_MSG_RESULT([disabled])
|
||||
fi
|
||||
|
||||
AC_SUBST(SCOREFILE)
|
||||
|
||||
AC_ARG_ENABLE([lockfile],[AC_HELP_STRING([--enable-lockfile=LOCKFILE], [enable scoreboard lockfile with given filename])],[],[])
|
||||
AC_MSG_CHECKING([for scoreboard lockfile file])
|
||||
if test "x$enable_lockfile" = "xno" ; then
|
||||
LOCKFILE=
|
||||
elif test "x$enable_lockfile" = "xyes" ; then
|
||||
LOCKFILE=$progname.lck
|
||||
elif test "x$enable_lockfile" = "x" ; then
|
||||
LOCKFILE=$progname.lck
|
||||
else
|
||||
LOCKFILE=$enable_lockfile
|
||||
fi
|
||||
|
||||
if test "x$LOCKFILE" != "x" ; then
|
||||
AC_DEFINE_UNQUOTED([LOCKFILE], ["$LOCKFILE"], [Define to file to use for scoreboard lockfile])
|
||||
AC_MSG_RESULT([$LOCKFILE])
|
||||
else
|
||||
AC_MSG_RESULT([disabled])
|
||||
fi
|
||||
|
||||
AC_SUBST(LOCKFILE)
|
||||
|
||||
AC_ARG_ENABLE([wizardmode],[AC_HELP_STRING([--enable-wizardmode], [enable availability of wizard mode @<:@default=no@:>@])],[],[])
|
||||
AC_MSG_CHECKING([if wizard mode is enabled])
|
||||
if test "x$enable_wizardmode" = "xno" ; then
|
||||
AC_MSG_RESULT([no])
|
||||
elif test "x$enable_wizardmode" = "x" ; then
|
||||
AC_MSG_RESULT([no])
|
||||
else
|
||||
AC_DEFINE([MASTER], [], [Define to include wizard mode])
|
||||
if test "x$enable_wizardmode" != "xyes" ; then
|
||||
AC_DEFINE_UNQUOTED([PASSWD],[$enable_wizardmode], [Define crypt(3) wizard mode password])
|
||||
fi
|
||||
AC_MSG_RESULT([yes])
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE([allscores],[AC_HELP_STRING([--enable-allscores], [enable scoreboard to show top scores, not just top players @<:@default=yes@:>@])],[],[enable_allscores=yes])
|
||||
AC_MSG_CHECKING([if allscores is enabled])
|
||||
if test "x$enable_allscores" = "xyes" ; then
|
||||
AC_DEFINE([ALLSCORES], [1], [Define if scorefile is top scores, not top players])
|
||||
AC_MSG_RESULT([yes])
|
||||
else
|
||||
AC_MSG_RESULT([no])
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE([checktime],[AC_HELP_STRING([--enable-checktime], [enable checktime @<:@default=no@:>@])],[],[])
|
||||
AC_MSG_CHECKING([if checktime is enabled])
|
||||
if test "x$enable_checktime" = "xyes" ; then
|
||||
AC_DEFINE([CHECKTIME], [1], [Define if checktime feature should be enabled])
|
||||
AC_MSG_RESULT([yes])
|
||||
else
|
||||
AC_MSG_RESULT([no])
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE([maxload],[AC_HELP_STRING([--enable-maxload], [enable maxload @<:@default=no@:>@])],[],[])
|
||||
AC_MSG_CHECKING([runtime execution limit (maximum system load average)])
|
||||
if test "x$enable_maxload" = "xyes" ; then
|
||||
AC_DEFINE([MAXLOAD], [100], [Define if maxload feature should be enabled])
|
||||
AC_MSG_RESULT([100])
|
||||
elif test "x$enable_maxload" = "x" ; then
|
||||
AC_MSG_RESULT([unlimited])
|
||||
elif test "x$enable_maxload" = "xno" ; then
|
||||
AC_MSG_RESULT([unlimited])
|
||||
else
|
||||
AC_DEFINE_UNQUOTED([MAXLOAD], [$enable_maxload], [Define if maxload feature should be enabled])
|
||||
AC_MSG_RESULT([$enable_maxload])
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE([maxusers],[AC_HELP_STRING([--enable-maxusers], [enable maxuser @<:@default=no@:>@])],[],[])
|
||||
AC_MSG_CHECKING([runtime execution limit (maximum online system users)])
|
||||
if test "x$enable_maxusers" = "xyes" ; then
|
||||
AC_DEFINE([MAXUSERS], [100], [Define if maxusers feature should be enabled])
|
||||
AC_MSG_RESULT([100])
|
||||
elif test "x$enable_maxusers" = "x" ; then
|
||||
AC_MSG_RESULT([unlimited])
|
||||
elif test "x$enable_maxload" = "xno" ; then
|
||||
AC_MSG_RESULT([unlimited])
|
||||
else
|
||||
AC_DEFINE_UNQUOTED([MAXLOAD], [$enable_maxusers], [Define if maxusers feature should be enabled])
|
||||
AC_MSG_RESULT([$enable_maxusers])
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE([numscores],[AC_HELP_STRING([--enable-numscores], [number of scores to store in scoreboard @<:@default=10@:>@])],[],[])
|
||||
AC_MSG_CHECKING([what the number of scores to store in scoreboard is])
|
||||
if test "x$numscores" = "xyes" ; then
|
||||
AC_DEFINE([NUMSCORES], [10], [number of scores to store in scoreboard])
|
||||
AC_MSG_RESULT([10])
|
||||
elif test "x$enable_numscores" = "x" ; then
|
||||
AC_DEFINE([NUMSCORES], [10], [number of scores to store in scoreboard])
|
||||
AC_MSG_RESULT([10])
|
||||
elif test "x$enable_numscores" = "xno" ; then
|
||||
AC_DEFINE([NUMSCORES], [10], [number of scores to store in scoreboard])
|
||||
AC_MSG_RESULT([10])
|
||||
else
|
||||
AC_DEFINE_UNQUOTED([NUMSCORES], [$enable_numscores], [number of scores to store in scoreboard])
|
||||
AC_MSG_RESULT([$enable_numscores])
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE([numname],[AC_HELP_STRING([--enable-numname], [word for number of scores to store in scoreboard @<:@default=Ten@:>@])],[],[])
|
||||
AC_MSG_CHECKING([word for the number of scores to store in scoreboard is])
|
||||
if test "x$enable_numname" = "xyes" ; then
|
||||
AC_DEFINE([NUMNAME], ["Ten"], [word for the number of scores to store in scoreboard])
|
||||
AC_MSG_RESULT([Ten])
|
||||
elif test "x$enable_numname" = "x" ; then
|
||||
AC_DEFINE([NUMNAME], ["Ten"], [word for the number of scores to store in scoreboard])
|
||||
AC_MSG_RESULT([Ten])
|
||||
elif test "x$enable_numname" = "xno" ; then
|
||||
AC_DEFINE([NUMNAME], ["Ten"], [word for the number of scores to store in scoreboard])
|
||||
AC_MSG_RESULT([Ten])
|
||||
else
|
||||
AC_DEFINE_UNQUOTED([NUMNAME], ["$enable_numname"], [word for the number of scores to store in scoreboard])
|
||||
AC_MSG_RESULT([$enable_numname])
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE([loadav],[AC_HELP_STRING([--enable-loadav=NAMELIST], [use program's load average function (unlikely to work) @<:@default=no@:>@])],[],[])
|
||||
AC_MSG_CHECKING([whether to use program's built in load average function])
|
||||
if test "x$enable_loadav" = "xyes" ; then
|
||||
AC_DEFINE([LOADAV], [], [define if we should use program's load average function instead of system])
|
||||
AC_DEFINE([NAMELIST], [/vmunix], [kernel file to pass to nlist() when reading load average (unlikely to work)])
|
||||
AC_MSG_RESULT([/vmunix])
|
||||
elif test "x$enable_loadav" = "x" ; then
|
||||
AC_MSG_RESULT([no])
|
||||
elif test "x$enable_loadav" = "xno" ; then
|
||||
AC_MSG_RESULT([no])
|
||||
else
|
||||
AC_DEFINE([LOADAV], [], [define if we should use program's load average function instead of system])
|
||||
AC_DEFINE_UNQUOTED([NAMELIST], [$enable_loadav], [kernel file to pass to nlist() when reading load average (unlikely to work)])
|
||||
AC_MSG_RESULT([$enable_loadav])
|
||||
fi
|
||||
|
||||
AC_ARG_ENABLE([ucount],[AC_HELP_STRING([--enable-ucount=UTMPFILE], [use program's own function to count users (unlikely to work) @<:@default=no@:>@])],[],[])
|
||||
AC_MSG_CHECKING([whether to use program's built in user counting function])
|
||||
if test "x$enable_ucount" = "xyes" ; then
|
||||
AC_DEFINE([UCOUNT], [], [define if we should use program's user counting function instead of system's])
|
||||
AC_DEFINE([UTMP], [/etc/utmp], [utmp like file to pass to ucount() when counting online users (unlikely to work)])
|
||||
AC_MSG_RESULT([/etc/utmp])
|
||||
elif test "x$enable_ucount" = "x" ; then
|
||||
AC_MSG_RESULT([no])
|
||||
elif test "x$enable_count" = "xno" ; then
|
||||
AC_MSG_RESULT([no])
|
||||
else
|
||||
AC_DEFINE([UCOUNT], [], [define if we should use program's user counting function instead of system's])
|
||||
AC_DEFINE_UNQUOTED([UTMP], [$enable_ucount], [utmp like file to pass to ucount() when counting online users (unlikely to work)])
|
||||
AC_MSG_RESULT([$enable_ucount])
|
||||
fi
|
||||
|
||||
TARGET=$target
|
||||
AC_SUBST(TARGET)
|
||||
|
||||
AC_MSG_CHECKING([whether to docdir is defined])
|
||||
if test "x$docdir" = "x" ; then
|
||||
AC_MSG_RESULT([docdir undefined])
|
||||
docdir=\${datadir}/doc/\${PACKAGE_TARNAME}
|
||||
AC_SUBST(docdir)
|
||||
else
|
||||
AC_MSG_RESULT([docdir defined])
|
||||
fi
|
||||
|
||||
AC_OUTPUT
|
||||
181
daemon.c
Normal file
181
daemon.c
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Contains functions for dealing with things that happen in the
|
||||
* future.
|
||||
*
|
||||
* @(#)daemon.c 4.7 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
#define EMPTY 0
|
||||
#define DAEMON -1
|
||||
#define MAXDAEMONS 20
|
||||
|
||||
#define _X_ { EMPTY }
|
||||
|
||||
struct delayed_action d_list[MAXDAEMONS] = {
|
||||
_X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_,
|
||||
_X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_,
|
||||
};
|
||||
|
||||
/*
|
||||
* d_slot:
|
||||
* Find an empty slot in the daemon/fuse list
|
||||
*/
|
||||
struct delayed_action *
|
||||
d_slot()
|
||||
{
|
||||
register struct delayed_action *dev;
|
||||
|
||||
for (dev = d_list; dev <= &d_list[MAXDAEMONS-1]; dev++)
|
||||
if (dev->d_type == EMPTY)
|
||||
return dev;
|
||||
#ifdef MASTER
|
||||
debug("Ran out of fuse slots");
|
||||
#endif
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* find_slot:
|
||||
* Find a particular slot in the table
|
||||
*/
|
||||
struct delayed_action *
|
||||
find_slot(void (*func)())
|
||||
{
|
||||
register struct delayed_action *dev;
|
||||
|
||||
for (dev = d_list; dev <= &d_list[MAXDAEMONS-1]; dev++)
|
||||
if (dev->d_type != EMPTY && func == dev->d_func)
|
||||
return dev;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* start_daemon:
|
||||
* Start a daemon, takes a function.
|
||||
*/
|
||||
void
|
||||
start_daemon(void (*func)(), int arg, int type)
|
||||
{
|
||||
register struct delayed_action *dev;
|
||||
|
||||
dev = d_slot();
|
||||
dev->d_type = type;
|
||||
dev->d_func = func;
|
||||
dev->d_arg = arg;
|
||||
dev->d_time = DAEMON;
|
||||
}
|
||||
|
||||
/*
|
||||
* kill_daemon:
|
||||
* Remove a daemon from the list
|
||||
*/
|
||||
void
|
||||
kill_daemon(void (*func)())
|
||||
{
|
||||
register struct delayed_action *dev;
|
||||
|
||||
if ((dev = find_slot(func)) == NULL)
|
||||
return;
|
||||
/*
|
||||
* Take it out of the list
|
||||
*/
|
||||
dev->d_type = EMPTY;
|
||||
}
|
||||
|
||||
/*
|
||||
* do_daemons:
|
||||
* Run all the daemons that are active with the current flag,
|
||||
* passing the argument to the function.
|
||||
*/
|
||||
void
|
||||
do_daemons(int flag)
|
||||
{
|
||||
register struct delayed_action *dev;
|
||||
|
||||
/*
|
||||
* Loop through the devil list
|
||||
*/
|
||||
for (dev = d_list; dev <= &d_list[MAXDAEMONS-1]; dev++)
|
||||
/*
|
||||
* Executing each one, giving it the proper arguments
|
||||
*/
|
||||
if (dev->d_type == flag && dev->d_time == DAEMON)
|
||||
(*dev->d_func)(dev->d_arg);
|
||||
}
|
||||
|
||||
/*
|
||||
* fuse:
|
||||
* Start a fuse to go off in a certain number of turns
|
||||
*/
|
||||
void
|
||||
fuse(void (*func)(), int arg, int time, int type)
|
||||
{
|
||||
register struct delayed_action *wire;
|
||||
|
||||
wire = d_slot();
|
||||
wire->d_type = type;
|
||||
wire->d_func = func;
|
||||
wire->d_arg = arg;
|
||||
wire->d_time = time;
|
||||
}
|
||||
|
||||
/*
|
||||
* lengthen:
|
||||
* Increase the time until a fuse goes off
|
||||
*/
|
||||
void
|
||||
lengthen(void (*func)(), int xtime)
|
||||
{
|
||||
register struct delayed_action *wire;
|
||||
|
||||
if ((wire = find_slot(func)) == NULL)
|
||||
return;
|
||||
wire->d_time += xtime;
|
||||
}
|
||||
|
||||
/*
|
||||
* extinguish:
|
||||
* Put out a fuse
|
||||
*/
|
||||
void
|
||||
extinguish(void (*func)())
|
||||
{
|
||||
register struct delayed_action *wire;
|
||||
|
||||
if ((wire = find_slot(func)) == NULL)
|
||||
return;
|
||||
wire->d_type = EMPTY;
|
||||
}
|
||||
|
||||
/*
|
||||
* do_fuses:
|
||||
* Decrement counters and start needed fuses
|
||||
*/
|
||||
void
|
||||
do_fuses(int flag)
|
||||
{
|
||||
register struct delayed_action *wire;
|
||||
|
||||
/*
|
||||
* Step though the list
|
||||
*/
|
||||
for (wire = d_list; wire <= &d_list[MAXDAEMONS-1]; wire++)
|
||||
/*
|
||||
* Decrementing counters and starting things we want. We also need
|
||||
* to remove the fuse from the list once it has gone off.
|
||||
*/
|
||||
if (flag == wire->d_type && wire->d_time > 0 && --wire->d_time == 0)
|
||||
{
|
||||
wire->d_type = EMPTY;
|
||||
(*wire->d_func)(wire->d_arg);
|
||||
}
|
||||
}
|
||||
295
daemons.c
Normal file
295
daemons.c
Normal file
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* All the daemon and fuse functions are in here
|
||||
*
|
||||
* @(#)daemons.c 4.24 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* doctor:
|
||||
* A healing daemon that restors hit points after rest
|
||||
*/
|
||||
void
|
||||
doctor()
|
||||
{
|
||||
register int lv, ohp;
|
||||
|
||||
lv = pstats.s_lvl;
|
||||
ohp = pstats.s_hpt;
|
||||
quiet++;
|
||||
if (lv < 8)
|
||||
{
|
||||
if (quiet + (lv << 1) > 20)
|
||||
pstats.s_hpt++;
|
||||
}
|
||||
else
|
||||
if (quiet >= 3)
|
||||
pstats.s_hpt += rnd(lv - 7) + 1;
|
||||
if (ISRING(LEFT, R_REGEN))
|
||||
pstats.s_hpt++;
|
||||
if (ISRING(RIGHT, R_REGEN))
|
||||
pstats.s_hpt++;
|
||||
if (ohp != pstats.s_hpt)
|
||||
{
|
||||
if (pstats.s_hpt > max_hp)
|
||||
pstats.s_hpt = max_hp;
|
||||
quiet = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Swander:
|
||||
* Called when it is time to start rolling for wandering monsters
|
||||
*/
|
||||
void
|
||||
swander()
|
||||
{
|
||||
start_daemon(rollwand, 0, BEFORE);
|
||||
}
|
||||
|
||||
/*
|
||||
* rollwand:
|
||||
* Called to roll to see if a wandering monster starts up
|
||||
*/
|
||||
int between = 0;
|
||||
void
|
||||
rollwand()
|
||||
{
|
||||
|
||||
if (++between >= 4)
|
||||
{
|
||||
if (roll(1, 6) == 4)
|
||||
{
|
||||
wanderer();
|
||||
kill_daemon(rollwand);
|
||||
fuse(swander, 0, WANDERTIME, BEFORE);
|
||||
}
|
||||
between = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* unconfuse:
|
||||
* Release the poor player from his confusion
|
||||
*/
|
||||
void
|
||||
unconfuse()
|
||||
{
|
||||
player.t_flags &= ~ISHUH;
|
||||
msg("you feel less %s now", choose_str("trippy", "confused"));
|
||||
}
|
||||
|
||||
/*
|
||||
* unsee:
|
||||
* Turn off the ability to see invisible
|
||||
*/
|
||||
void
|
||||
unsee()
|
||||
{
|
||||
register THING *th;
|
||||
|
||||
for (th = mlist; th != NULL; th = next(th))
|
||||
if (on(*th, ISINVIS) && see_monst(th))
|
||||
mvaddch(th->t_pos.y, th->t_pos.x, th->t_oldch);
|
||||
player.t_flags &= ~CANSEE;
|
||||
}
|
||||
|
||||
/*
|
||||
* sight:
|
||||
* He gets his sight back
|
||||
*/
|
||||
void
|
||||
sight()
|
||||
{
|
||||
if (on(player, ISBLIND))
|
||||
{
|
||||
extinguish(sight);
|
||||
player.t_flags &= ~ISBLIND;
|
||||
if (!(proom->r_flags & ISGONE))
|
||||
enter_room(&hero);
|
||||
msg(choose_str("far out! Everything is all cosmic again",
|
||||
"the veil of darkness lifts"));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* nohaste:
|
||||
* End the hasting
|
||||
*/
|
||||
void
|
||||
nohaste()
|
||||
{
|
||||
player.t_flags &= ~ISHASTE;
|
||||
msg("you feel yourself slowing down");
|
||||
}
|
||||
|
||||
/*
|
||||
* stomach:
|
||||
* Digest the hero's food
|
||||
*/
|
||||
void
|
||||
stomach()
|
||||
{
|
||||
register int oldfood;
|
||||
int orig_hungry = hungry_state;
|
||||
|
||||
if (food_left <= 0)
|
||||
{
|
||||
if (food_left-- < -STARVETIME)
|
||||
death('s');
|
||||
/*
|
||||
* the hero is fainting
|
||||
*/
|
||||
if (no_command || rnd(5) != 0)
|
||||
return;
|
||||
no_command += rnd(8) + 4;
|
||||
hungry_state = 3;
|
||||
if (!terse)
|
||||
addmsg(choose_str("the munchies overpower your motor capabilities. ",
|
||||
"you feel too weak from lack of food. "));
|
||||
msg(choose_str("You freak out", "You faint"));
|
||||
}
|
||||
else
|
||||
{
|
||||
oldfood = food_left;
|
||||
food_left -= ring_eat(LEFT) + ring_eat(RIGHT) + 1 - amulet;
|
||||
|
||||
if (food_left < MORETIME && oldfood >= MORETIME)
|
||||
{
|
||||
hungry_state = 2;
|
||||
msg(choose_str("the munchies are interfering with your motor capabilites",
|
||||
"you are starting to feel weak"));
|
||||
}
|
||||
else if (food_left < 2 * MORETIME && oldfood >= 2 * MORETIME)
|
||||
{
|
||||
hungry_state = 1;
|
||||
if (terse)
|
||||
msg(choose_str("getting the munchies", "getting hungry"));
|
||||
else
|
||||
msg(choose_str("you are getting the munchies",
|
||||
"you are starting to get hungry"));
|
||||
}
|
||||
}
|
||||
if (hungry_state != orig_hungry) {
|
||||
player.t_flags &= ~ISRUN;
|
||||
running = FALSE;
|
||||
to_death = FALSE;
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* come_down:
|
||||
* Take the hero down off her acid trip.
|
||||
*/
|
||||
void
|
||||
come_down()
|
||||
{
|
||||
register THING *tp;
|
||||
register bool seemonst;
|
||||
|
||||
if (!on(player, ISHALU))
|
||||
return;
|
||||
|
||||
kill_daemon(visuals);
|
||||
player.t_flags &= ~ISHALU;
|
||||
|
||||
if (on(player, ISBLIND))
|
||||
return;
|
||||
|
||||
/*
|
||||
* undo the things
|
||||
*/
|
||||
for (tp = lvl_obj; tp != NULL; tp = next(tp))
|
||||
if (cansee(tp->o_pos.y, tp->o_pos.x))
|
||||
mvaddch(tp->o_pos.y, tp->o_pos.x, tp->o_type);
|
||||
|
||||
/*
|
||||
* undo the monsters
|
||||
*/
|
||||
seemonst = on(player, SEEMONST);
|
||||
for (tp = mlist; tp != NULL; tp = next(tp))
|
||||
{
|
||||
move(tp->t_pos.y, tp->t_pos.x);
|
||||
if (cansee(tp->t_pos.y, tp->t_pos.x))
|
||||
if (!on(*tp, ISINVIS) || on(player, CANSEE))
|
||||
addch(tp->t_disguise);
|
||||
else
|
||||
addch(chat(tp->t_pos.y, tp->t_pos.x));
|
||||
else if (seemonst)
|
||||
{
|
||||
standout();
|
||||
addch(tp->t_type);
|
||||
standend();
|
||||
}
|
||||
}
|
||||
msg("Everything looks SO boring now.");
|
||||
}
|
||||
|
||||
/*
|
||||
* visuals:
|
||||
* change the characters for the player
|
||||
*/
|
||||
void
|
||||
visuals()
|
||||
{
|
||||
register THING *tp;
|
||||
register bool seemonst;
|
||||
|
||||
if (!after || (running && jump))
|
||||
return;
|
||||
/*
|
||||
* change the things
|
||||
*/
|
||||
for (tp = lvl_obj; tp != NULL; tp = next(tp))
|
||||
if (cansee(tp->o_pos.y, tp->o_pos.x))
|
||||
mvaddch(tp->o_pos.y, tp->o_pos.x, rnd_thing());
|
||||
|
||||
/*
|
||||
* change the stairs
|
||||
*/
|
||||
if (!seenstairs && cansee(stairs.y, stairs.x))
|
||||
mvaddch(stairs.y, stairs.x, rnd_thing());
|
||||
|
||||
/*
|
||||
* change the monsters
|
||||
*/
|
||||
seemonst = on(player, SEEMONST);
|
||||
for (tp = mlist; tp != NULL; tp = next(tp))
|
||||
{
|
||||
move(tp->t_pos.y, tp->t_pos.x);
|
||||
if (see_monst(tp))
|
||||
{
|
||||
if (tp->t_type == 'X' && tp->t_disguise != 'X')
|
||||
addch(rnd_thing());
|
||||
else
|
||||
addch(rnd(26) + 'A');
|
||||
}
|
||||
else if (seemonst)
|
||||
{
|
||||
standout();
|
||||
addch(rnd(26) + 'A');
|
||||
standend();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* land:
|
||||
* Land from a levitation potion
|
||||
*/
|
||||
void
|
||||
land()
|
||||
{
|
||||
player.t_flags &= ~ISLEVIT;
|
||||
msg(choose_str("bummer! You've hit the ground",
|
||||
"you float gently to the ground"));
|
||||
}
|
||||
391
extern.c
Normal file
391
extern.c
Normal file
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* global variable initializaton
|
||||
*
|
||||
* @(#)extern.c 4.82 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
bool after; /* True if we want after daemons */
|
||||
bool again; /* Repeating the last command */
|
||||
int noscore; /* Was a wizard sometime */
|
||||
bool seenstairs; /* Have seen the stairs (for lsd) */
|
||||
bool amulet = FALSE; /* He found the amulet */
|
||||
bool door_stop = FALSE; /* Stop running when we pass a door */
|
||||
bool fight_flush = FALSE; /* True if toilet input */
|
||||
bool firstmove = FALSE; /* First move after setting door_stop */
|
||||
bool got_ltc = FALSE; /* We have gotten the local tty chars */
|
||||
bool has_hit = FALSE; /* Has a "hit" message pending in msg */
|
||||
bool in_shell = FALSE; /* True if executing a shell */
|
||||
bool inv_describe = TRUE; /* Say which way items are being used */
|
||||
bool jump = FALSE; /* Show running as series of jumps */
|
||||
bool kamikaze = FALSE; /* to_death really to DEATH */
|
||||
bool lower_msg = FALSE; /* Messages should start w/lower case */
|
||||
bool move_on = FALSE; /* Next move shouldn't pick up items */
|
||||
bool msg_esc = FALSE; /* Check for ESC from msg's --More-- */
|
||||
bool passgo = FALSE; /* Follow passages */
|
||||
bool playing = TRUE; /* True until he quits */
|
||||
bool q_comm = FALSE; /* Are we executing a 'Q' command? */
|
||||
bool running = FALSE; /* True if player is running */
|
||||
bool save_msg = TRUE; /* Remember last msg */
|
||||
bool see_floor = TRUE; /* Show the lamp illuminated floor */
|
||||
bool stat_msg = FALSE; /* Should status() print as a msg() */
|
||||
bool terse = FALSE; /* True if we should be short */
|
||||
bool to_death = FALSE; /* Fighting is to the death! */
|
||||
bool tombstone = TRUE; /* Print out tombstone at end */
|
||||
#ifdef MASTER
|
||||
int wizard = FALSE; /* True if allows wizard commands */
|
||||
#endif
|
||||
bool pack_used[26] = { /* Is the character used in the pack? */
|
||||
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
|
||||
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE,
|
||||
FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE
|
||||
};
|
||||
|
||||
char dir_ch; /* Direction from last get_dir() call */
|
||||
char file_name[MAXSTR]; /* Save file name */
|
||||
char huh[MAXSTR]; /* The last message printed */
|
||||
char *p_colors[MAXPOTIONS]; /* Colors of the potions */
|
||||
char prbuf[2*MAXSTR]; /* buffer for sprintfs */
|
||||
char *r_stones[MAXRINGS]; /* Stone settings of the rings */
|
||||
char runch; /* Direction player is running */
|
||||
char *s_names[MAXSCROLLS]; /* Names of the scrolls */
|
||||
char take; /* Thing she is taking */
|
||||
char whoami[MAXSTR]; /* Name of player */
|
||||
char *ws_made[MAXSTICKS]; /* What sticks are made of */
|
||||
char *ws_type[MAXSTICKS]; /* Is it a wand or a staff */
|
||||
int orig_dsusp; /* Original dsusp char */
|
||||
char fruit[MAXSTR] = /* Favorite fruit */
|
||||
{ 's', 'l', 'i', 'm', 'e', '-', 'm', 'o', 'l', 'd', '\0' };
|
||||
char home[MAXSTR] = { '\0' }; /* User's home directory */
|
||||
char *inv_t_name[] = {
|
||||
"Overwrite",
|
||||
"Slow",
|
||||
"Clear"
|
||||
};
|
||||
char l_last_comm = '\0'; /* Last last_comm */
|
||||
char l_last_dir = '\0'; /* Last last_dir */
|
||||
char last_comm = '\0'; /* Last command typed */
|
||||
char last_dir = '\0'; /* Last direction given */
|
||||
char *tr_name[] = { /* Names of the traps */
|
||||
"a trapdoor",
|
||||
"an arrow trap",
|
||||
"a sleeping gas trap",
|
||||
"a beartrap",
|
||||
"a teleport trap",
|
||||
"a poison dart trap",
|
||||
"a rust trap",
|
||||
"a mysterious trap"
|
||||
};
|
||||
|
||||
|
||||
int n_objs; /* # items listed in inventory() call */
|
||||
int ntraps; /* Number of traps on this level */
|
||||
int hungry_state = 0; /* How hungry is he */
|
||||
int inpack = 0; /* Number of things in pack */
|
||||
int inv_type = 0; /* Type of inventory to use */
|
||||
int level = 1; /* What level she is on */
|
||||
int max_hit; /* Max damage done to her in to_death */
|
||||
int max_level; /* Deepest player has gone */
|
||||
int mpos = 0; /* Where cursor is on top line */
|
||||
int no_food = 0; /* Number of levels without food */
|
||||
int a_class[MAXARMORS] = { /* Armor class for each armor type */
|
||||
8, /* LEATHER */
|
||||
7, /* RING_MAIL */
|
||||
7, /* STUDDED_LEATHER */
|
||||
6, /* SCALE_MAIL */
|
||||
5, /* CHAIN_MAIL */
|
||||
4, /* SPLINT_MAIL */
|
||||
4, /* BANDED_MAIL */
|
||||
3, /* PLATE_MAIL */
|
||||
};
|
||||
|
||||
int count = 0; /* Number of times to repeat command */
|
||||
FILE *scoreboard = NULL; /* File descriptor for score file */
|
||||
int food_left; /* Amount of food in hero's stomach */
|
||||
int lastscore = -1; /* Score before this turn */
|
||||
int no_command = 0; /* Number of turns asleep */
|
||||
int no_move = 0; /* Number of turns held in place */
|
||||
int purse = 0; /* How much gold he has */
|
||||
int quiet = 0; /* Number of quiet turns */
|
||||
int vf_hit = 0; /* Number of time flytrap has hit */
|
||||
|
||||
int dnum; /* Dungeon number */
|
||||
int seed; /* Random number seed */
|
||||
int e_levels[] = {
|
||||
10L,
|
||||
20L,
|
||||
40L,
|
||||
80L,
|
||||
160L,
|
||||
320L,
|
||||
640L,
|
||||
1300L,
|
||||
2600L,
|
||||
5200L,
|
||||
13000L,
|
||||
26000L,
|
||||
50000L,
|
||||
100000L,
|
||||
200000L,
|
||||
400000L,
|
||||
800000L,
|
||||
2000000L,
|
||||
4000000L,
|
||||
8000000L,
|
||||
0L
|
||||
};
|
||||
|
||||
coord delta; /* Change indicated to get_dir() */
|
||||
coord oldpos; /* Position before last look() call */
|
||||
coord stairs; /* Location of staircase */
|
||||
|
||||
PLACE places[MAXLINES*MAXCOLS]; /* level map */
|
||||
|
||||
THING *cur_armor; /* What he is wearing */
|
||||
THING *cur_ring[2]; /* Which rings are being worn */
|
||||
THING *cur_weapon; /* Which weapon he is weilding */
|
||||
THING *l_last_pick = NULL; /* Last last_pick */
|
||||
THING *last_pick = NULL; /* Last object picked in get_item() */
|
||||
THING *lvl_obj = NULL; /* List of objects on this level */
|
||||
THING *mlist = NULL; /* List of monsters on the level */
|
||||
THING player; /* His stats */
|
||||
/* restart of game */
|
||||
|
||||
WINDOW *hw = NULL; /* used as a scratch window */
|
||||
|
||||
#define INIT_STATS { 16, 0, 1, 10, 12, "1x4", 12 }
|
||||
|
||||
struct stats max_stats = INIT_STATS; /* The maximum for the player */
|
||||
|
||||
struct room *oldrp; /* Roomin(&oldpos) */
|
||||
struct room rooms[MAXROOMS]; /* One for each room -- A level */
|
||||
struct room passages[MAXPASS] = /* One for each passage */
|
||||
{
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} },
|
||||
{ {0, 0}, {0, 0}, {0, 0}, 0, ISGONE|ISDARK, 0, {{0,0}} }
|
||||
};
|
||||
|
||||
#define ___ 1
|
||||
#define XX 10
|
||||
struct monster monsters[26] =
|
||||
{
|
||||
/* Name CARRY FLAG str, exp, lvl, amr, hpt, dmg */
|
||||
{ "aquator", 0, ISMEAN, { XX, 20, 5, 2, ___, "0x0/0x0" } },
|
||||
{ "bat", 0, ISFLY, { XX, 1, 1, 3, ___, "1x2" } },
|
||||
{ "centaur", 15, 0, { XX, 17, 4, 4, ___, "1x2/1x5/1x5" } },
|
||||
{ "dragon", 100, ISMEAN, { XX,5000, 10, -1, ___, "1x8/1x8/3x10" } },
|
||||
{ "emu", 0, ISMEAN, { XX, 2, 1, 7, ___, "1x2" } },
|
||||
{ "venus flytrap", 0, ISMEAN, { XX, 80, 8, 3, ___, "%%%x0" } },
|
||||
/* NOTE: the damage is %%% so that xstr won't merge this */
|
||||
/* string with others, since it is written on in the program */
|
||||
{ "griffin", 20, ISMEAN|ISFLY|ISREGEN, { XX,2000, 13, 2, ___, "4x3/3x5" } },
|
||||
{ "hobgoblin", 0, ISMEAN, { XX, 3, 1, 5, ___, "1x8" } },
|
||||
{ "ice monster", 0, 0, { XX, 5, 1, 9, ___, "0x0" } },
|
||||
{ "jabberwock", 70, 0, { XX,3000, 15, 6, ___, "2x12/2x4" } },
|
||||
{ "kestrel", 0, ISMEAN|ISFLY, { XX, 1, 1, 7, ___, "1x4" } },
|
||||
{ "leprechaun", 0, 0, { XX, 10, 3, 8, ___, "1x1" } },
|
||||
{ "medusa", 40, ISMEAN, { XX,200, 8, 2, ___, "3x4/3x4/2x5" } },
|
||||
{ "nymph", 100, 0, { XX, 37, 3, 9, ___, "0x0" } },
|
||||
{ "orc", 15, ISGREED,{ XX, 5, 1, 6, ___, "1x8" } },
|
||||
{ "phantom", 0, ISINVIS,{ XX,120, 8, 3, ___, "4x4" } },
|
||||
{ "quagga", 0, ISMEAN, { XX, 15, 3, 3, ___, "1x5/1x5" } },
|
||||
{ "rattlesnake", 0, ISMEAN, { XX, 9, 2, 3, ___, "1x6" } },
|
||||
{ "snake", 0, ISMEAN, { XX, 2, 1, 5, ___, "1x3" } },
|
||||
{ "troll", 50, ISREGEN|ISMEAN,{ XX, 120, 6, 4, ___, "1x8/1x8/2x6" } },
|
||||
{ "black unicorn", 0, ISMEAN, { XX,190, 7, -2, ___, "1x9/1x9/2x9" } },
|
||||
{ "vampire", 20, ISREGEN|ISMEAN,{ XX,350, 8, 1, ___, "1x10" } },
|
||||
{ "wraith", 0, 0, { XX, 55, 5, 4, ___, "1x6" } },
|
||||
{ "xeroc", 30, 0, { XX,100, 7, 7, ___, "4x4" } },
|
||||
{ "yeti", 30, 0, { XX, 50, 4, 6, ___, "1x6/1x6" } },
|
||||
{ "zombie", 0, ISMEAN, { XX, 6, 2, 8, ___, "1x8" } }
|
||||
};
|
||||
#undef ___
|
||||
#undef XX
|
||||
|
||||
struct obj_info things[NUMTHINGS] = {
|
||||
{ 0, 26 }, /* potion */
|
||||
{ 0, 36 }, /* scroll */
|
||||
{ 0, 16 }, /* food */
|
||||
{ 0, 7 }, /* weapon */
|
||||
{ 0, 7 }, /* armor */
|
||||
{ 0, 4 }, /* ring */
|
||||
{ 0, 4 }, /* stick */
|
||||
};
|
||||
|
||||
struct obj_info arm_info[MAXARMORS] = {
|
||||
{ "leather armor", 20, 20, NULL, FALSE },
|
||||
{ "ring mail", 15, 25, NULL, FALSE },
|
||||
{ "studded leather armor", 15, 20, NULL, FALSE },
|
||||
{ "scale mail", 13, 30, NULL, FALSE },
|
||||
{ "chain mail", 12, 75, NULL, FALSE },
|
||||
{ "splint mail", 10, 80, NULL, FALSE },
|
||||
{ "banded mail", 10, 90, NULL, FALSE },
|
||||
{ "plate mail", 5, 150, NULL, FALSE },
|
||||
};
|
||||
struct obj_info pot_info[MAXPOTIONS] = {
|
||||
{ "confusion", 7, 5, NULL, FALSE },
|
||||
{ "hallucination", 8, 5, NULL, FALSE },
|
||||
{ "poison", 8, 5, NULL, FALSE },
|
||||
{ "gain strength", 13, 150, NULL, FALSE },
|
||||
{ "see invisible", 3, 100, NULL, FALSE },
|
||||
{ "healing", 13, 130, NULL, FALSE },
|
||||
{ "monster detection", 6, 130, NULL, FALSE },
|
||||
{ "magic detection", 6, 105, NULL, FALSE },
|
||||
{ "raise level", 2, 250, NULL, FALSE },
|
||||
{ "extra healing", 5, 200, NULL, FALSE },
|
||||
{ "haste self", 5, 190, NULL, FALSE },
|
||||
{ "restore strength", 13, 130, NULL, FALSE },
|
||||
{ "blindness", 5, 5, NULL, FALSE },
|
||||
{ "levitation", 6, 75, NULL, FALSE },
|
||||
};
|
||||
struct obj_info ring_info[MAXRINGS] = {
|
||||
{ "protection", 9, 400, NULL, FALSE },
|
||||
{ "add strength", 9, 400, NULL, FALSE },
|
||||
{ "sustain strength", 5, 280, NULL, FALSE },
|
||||
{ "searching", 10, 420, NULL, FALSE },
|
||||
{ "see invisible", 10, 310, NULL, FALSE },
|
||||
{ "adornment", 1, 10, NULL, FALSE },
|
||||
{ "aggravate monster", 10, 10, NULL, FALSE },
|
||||
{ "dexterity", 8, 440, NULL, FALSE },
|
||||
{ "increase damage", 8, 400, NULL, FALSE },
|
||||
{ "regeneration", 4, 460, NULL, FALSE },
|
||||
{ "slow digestion", 9, 240, NULL, FALSE },
|
||||
{ "teleportation", 5, 30, NULL, FALSE },
|
||||
{ "stealth", 7, 470, NULL, FALSE },
|
||||
{ "maintain armor", 5, 380, NULL, FALSE },
|
||||
};
|
||||
struct obj_info scr_info[MAXSCROLLS] = {
|
||||
{ "monster confusion", 7, 140, NULL, FALSE },
|
||||
{ "magic mapping", 4, 150, NULL, FALSE },
|
||||
{ "hold monster", 2, 180, NULL, FALSE },
|
||||
{ "sleep", 3, 5, NULL, FALSE },
|
||||
{ "enchant armor", 7, 160, NULL, FALSE },
|
||||
{ "identify potion", 10, 80, NULL, FALSE },
|
||||
{ "identify scroll", 10, 80, NULL, FALSE },
|
||||
{ "identify weapon", 6, 80, NULL, FALSE },
|
||||
{ "identify armor", 7, 100, NULL, FALSE },
|
||||
{ "identify ring, wand or staff", 10, 115, NULL, FALSE },
|
||||
{ "scare monster", 3, 200, NULL, FALSE },
|
||||
{ "food detection", 2, 60, NULL, FALSE },
|
||||
{ "teleportation", 5, 165, NULL, FALSE },
|
||||
{ "enchant weapon", 8, 150, NULL, FALSE },
|
||||
{ "create monster", 4, 75, NULL, FALSE },
|
||||
{ "remove curse", 7, 105, NULL, FALSE },
|
||||
{ "aggravate monsters", 3, 20, NULL, FALSE },
|
||||
{ "protect armor", 2, 250, NULL, FALSE },
|
||||
};
|
||||
struct obj_info weap_info[MAXWEAPONS + 1] = {
|
||||
{ "mace", 11, 8, NULL, FALSE },
|
||||
{ "long sword", 11, 15, NULL, FALSE },
|
||||
{ "short bow", 12, 15, NULL, FALSE },
|
||||
{ "arrow", 12, 1, NULL, FALSE },
|
||||
{ "dagger", 8, 3, NULL, FALSE },
|
||||
{ "two handed sword", 10, 75, NULL, FALSE },
|
||||
{ "dart", 12, 2, NULL, FALSE },
|
||||
{ "shuriken", 12, 5, NULL, FALSE },
|
||||
{ "spear", 12, 5, NULL, FALSE },
|
||||
{ NULL, 0 }, /* DO NOT REMOVE: fake entry for dragon's breath */
|
||||
};
|
||||
struct obj_info ws_info[MAXSTICKS] = {
|
||||
{ "light", 12, 250, NULL, FALSE },
|
||||
{ "invisibility", 6, 5, NULL, FALSE },
|
||||
{ "lightning", 3, 330, NULL, FALSE },
|
||||
{ "fire", 3, 330, NULL, FALSE },
|
||||
{ "cold", 3, 330, NULL, FALSE },
|
||||
{ "polymorph", 15, 310, NULL, FALSE },
|
||||
{ "magic missile", 10, 170, NULL, FALSE },
|
||||
{ "haste monster", 10, 5, NULL, FALSE },
|
||||
{ "slow monster", 11, 350, NULL, FALSE },
|
||||
{ "drain life", 9, 300, NULL, FALSE },
|
||||
{ "nothing", 1, 5, NULL, FALSE },
|
||||
{ "teleport away", 6, 340, NULL, FALSE },
|
||||
{ "teleport to", 6, 50, NULL, FALSE },
|
||||
{ "cancellation", 5, 280, NULL, FALSE },
|
||||
};
|
||||
|
||||
struct h_list helpstr[] = {
|
||||
{'?', " prints help", TRUE},
|
||||
{'/', " identify object", TRUE},
|
||||
{'h', " left", TRUE},
|
||||
{'j', " down", TRUE},
|
||||
{'k', " up", TRUE},
|
||||
{'l', " right", TRUE},
|
||||
{'y', " up & left", TRUE},
|
||||
{'u', " up & right", TRUE},
|
||||
{'b', " down & left", TRUE},
|
||||
{'n', " down & right", TRUE},
|
||||
{'H', " run left", FALSE},
|
||||
{'J', " run down", FALSE},
|
||||
{'K', " run up", FALSE},
|
||||
{'L', " run right", FALSE},
|
||||
{'Y', " run up & left", FALSE},
|
||||
{'U', " run up & right", FALSE},
|
||||
{'B', " run down & left", FALSE},
|
||||
{'N', " run down & right", FALSE},
|
||||
{CTRL('H'), " run left until adjacent", FALSE},
|
||||
{CTRL('J'), " run down until adjacent", FALSE},
|
||||
{CTRL('K'), " run up until adjacent", FALSE},
|
||||
{CTRL('L'), " run right until adjacent", FALSE},
|
||||
{CTRL('Y'), " run up & left until adjacent", FALSE},
|
||||
{CTRL('U'), " run up & right until adjacent", FALSE},
|
||||
{CTRL('B'), " run down & left until adjacent", FALSE},
|
||||
{CTRL('N'), " run down & right until adjacent", FALSE},
|
||||
{'\0', " <SHIFT><dir>: run that way", TRUE},
|
||||
{'\0', " <CTRL><dir>: run till adjacent", TRUE},
|
||||
{'f', "<dir> fight till death or near death", TRUE},
|
||||
{'t', "<dir> throw something", TRUE},
|
||||
{'m', "<dir> move onto without picking up", TRUE},
|
||||
{'z', "<dir> zap a wand in a direction", TRUE},
|
||||
{'^', "<dir> identify trap type", TRUE},
|
||||
{'s', " search for trap/secret door", TRUE},
|
||||
{'>', " go down a staircase", TRUE},
|
||||
{'<', " go up a staircase", TRUE},
|
||||
{'.', " rest for a turn", TRUE},
|
||||
{',', " pick something up", TRUE},
|
||||
{'i', " inventory", TRUE},
|
||||
{'I', " inventory single item", TRUE},
|
||||
{'q', " quaff potion", TRUE},
|
||||
{'r', " read scroll", TRUE},
|
||||
{'e', " eat food", TRUE},
|
||||
{'w', " wield a weapon", TRUE},
|
||||
{'W', " wear armor", TRUE},
|
||||
{'T', " take armor off", TRUE},
|
||||
{'P', " put on ring", TRUE},
|
||||
{'R', " remove ring", TRUE},
|
||||
{'d', " drop object", TRUE},
|
||||
{'c', " call object", TRUE},
|
||||
{'a', " repeat last command", TRUE},
|
||||
{')', " print current weapon", TRUE},
|
||||
{']', " print current armor", TRUE},
|
||||
{'=', " print current rings", TRUE},
|
||||
{'@', " print current stats", TRUE},
|
||||
{'D', " recall what's been discovered", TRUE},
|
||||
{'o', " examine/set options", TRUE},
|
||||
{CTRL('R'), " redraw screen", TRUE},
|
||||
{CTRL('P'), " repeat last message", TRUE},
|
||||
{ESCAPE, " cancel command", TRUE},
|
||||
{'S', " save game", TRUE},
|
||||
{'Q', " quit", TRUE},
|
||||
{'!', " shell escape", TRUE},
|
||||
{'F', "<dir> fight till either of you dies", TRUE},
|
||||
{'v', " print version number", TRUE},
|
||||
{0, NULL }
|
||||
};
|
||||
197
extern.h
Normal file
197
extern.h
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Defines for things used in mach_dep.c
|
||||
*
|
||||
* @(#)extern.h 4.35 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#ifdef PDCURSES
|
||||
#undef HAVE_UNISTD_H
|
||||
#undef HAVE_LIMITS_H
|
||||
#undef HAVE_MEMORY_H
|
||||
#undef HAVE_STRING_H
|
||||
#endif
|
||||
#include "config.h"
|
||||
#elif defined(__DJGPP__)
|
||||
#define HAVE_SYS_TYPES_H 1
|
||||
#define HAVE_PROCESS_H 1
|
||||
#define HAVE_PWD_H 1
|
||||
#define HAVE_TERMIOS_H 1
|
||||
#define HAVE_SETGID 1
|
||||
#define HAVE_GETGID 1
|
||||
#define HAVE_SETUID 1
|
||||
#define HAVE_GETUID 1
|
||||
#define HAVE_GETPASS 1
|
||||
#define HAVE_SPAWNL 1
|
||||
#define HAVE_ALARM 1
|
||||
#define HAVE_ERASECHAR 1
|
||||
#define HAVE_KILLCHAR 1
|
||||
#elif defined(_WIN32)
|
||||
#define HAVE_CURSES_H
|
||||
#define HAVE_TERM_H
|
||||
#define HAVE__SPAWNL
|
||||
#define HAVE_SYS_TYPES_H
|
||||
#define HAVE_PROCESS_H
|
||||
#define HAVE_ERASECHAR 1
|
||||
#define HAVE_KILLCHAR 1
|
||||
#elif defined(__CYGWIN__)
|
||||
#define HAVE_SYS_TYPES_H 1
|
||||
#define HAVE_PWD_H 1
|
||||
#define HAVE_PWD_H 1
|
||||
#define HAVE_SYS_UTSNAME_H 1
|
||||
#define HAVE_ARPA_INET_H 1
|
||||
#define HAVE_UNISTD_H 1
|
||||
#define HAVE_TERMIOS_H 1
|
||||
#define HAVE_NCURSES_TERM_H 1
|
||||
#define HAVE_ESCDELAY
|
||||
#define HAVE_SETGID 1
|
||||
#define HAVE_GETGID 1
|
||||
#define HAVE_SETUID 1
|
||||
#define HAVE_GETUID 1
|
||||
#define HAVE_GETPASS 1
|
||||
#define HAVE_GETPWUID 1
|
||||
#define HAVE_WORKING_FORK 1
|
||||
#define HAVE_ALARM 1
|
||||
#define HAVE_SPAWNL 1
|
||||
#define HAVE__SPAWNL 1
|
||||
#define HAVE_ERASECHAR 1
|
||||
#define HAVE_KILLCHAR 1
|
||||
#else /* POSIX */
|
||||
#define HAVE_SYS_TYPES_H 1
|
||||
#define HAVE_PWD_H 1
|
||||
#define HAVE_PWD_H 1
|
||||
#define HAVE_SYS_UTSNAME_H 1
|
||||
#define HAVE_ARPA_INET_H 1
|
||||
#define HAVE_UNISTD_H 1
|
||||
#define HAVE_TERMIOS_H 1
|
||||
#define HAVE_TERM_H 1
|
||||
#define HAVE_SETGID 1
|
||||
#define HAVE_GETGID 1
|
||||
#define HAVE_SETUID 1
|
||||
#define HAVE_GETUID 1
|
||||
#define HAVE_SETREUID 1
|
||||
#define HAVE_SETREGID 1
|
||||
#define HAVE_GETPASS 1
|
||||
#define HAVE_GETPWUID 1
|
||||
#define HAVE_WORKING_FORK 1
|
||||
#define HAVE_ERASECHAR 1
|
||||
#define HAVE_KILLCHAR 1
|
||||
#ifndef _AIX
|
||||
#define HAVE_GETLOADAVG 1
|
||||
#endif
|
||||
#define HAVE_ALARM 1
|
||||
#endif
|
||||
|
||||
#ifdef __DJGPP__
|
||||
#undef HAVE_GETPWUID /* DJGPP's limited version doesn't even work as documented */
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Don't change the constants, since they are used for sizes in many
|
||||
* places in the program.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#undef SIGTSTP
|
||||
|
||||
#define MAXSTR 1024 /* maximum length of strings */
|
||||
#define MAXLINES 32 /* maximum number of screen lines used */
|
||||
#define MAXCOLS 80 /* maximum number of screen columns used */
|
||||
|
||||
#define RN (((seed = seed*11109+13849) >> 16) & 0xffff)
|
||||
#ifdef CTRL
|
||||
#undef CTRL
|
||||
#endif
|
||||
#define CTRL(c) (c & 037)
|
||||
|
||||
/*
|
||||
* Now all the global variables
|
||||
*/
|
||||
|
||||
extern bool got_ltc, in_shell;
|
||||
extern int wizard;
|
||||
extern char fruit[], prbuf[], whoami[];
|
||||
extern int orig_dsusp;
|
||||
extern FILE *scoreboard;
|
||||
|
||||
/*
|
||||
* Function types
|
||||
*/
|
||||
|
||||
void auto_save(int);
|
||||
void come_down();
|
||||
void doctor();
|
||||
void end_line();
|
||||
void endit(int sig);
|
||||
void fatal();
|
||||
void getltchars();
|
||||
void land();
|
||||
void leave(int);
|
||||
void my_exit();
|
||||
void nohaste();
|
||||
void playit();
|
||||
void playltchars(void);
|
||||
void print_disc(char);
|
||||
void quit(int);
|
||||
void resetltchars(void);
|
||||
void rollwand();
|
||||
void runners();
|
||||
void set_order();
|
||||
void sight();
|
||||
void stomach();
|
||||
void swander();
|
||||
void tstp(int ignored);
|
||||
void unconfuse();
|
||||
void unsee();
|
||||
void visuals();
|
||||
|
||||
char add_line(char *fmt, char *arg);
|
||||
|
||||
char *killname(char monst, bool doart);
|
||||
char *nothing(char type);
|
||||
char *type_name(int type);
|
||||
|
||||
#ifdef CHECKTIME
|
||||
int checkout();
|
||||
#endif
|
||||
|
||||
int md_chmod(char *filename, int mode);
|
||||
char *md_crypt(char *key, char *salt);
|
||||
int md_dsuspchar();
|
||||
int md_erasechar();
|
||||
char *md_gethomedir();
|
||||
char *md_getusername();
|
||||
int md_getuid();
|
||||
char *md_getpass(char *prompt);
|
||||
int md_getpid();
|
||||
char *md_getrealname(int uid);
|
||||
void md_init();
|
||||
int md_killchar();
|
||||
void md_normaluser();
|
||||
void md_raw_standout();
|
||||
void md_raw_standend();
|
||||
int md_readchar();
|
||||
int md_setdsuspchar(int c);
|
||||
int md_shellescape();
|
||||
void md_sleep(int s);
|
||||
int md_suspchar();
|
||||
int md_hasclreol();
|
||||
int md_unlink(char *file);
|
||||
int md_unlink_open_file(char *file, FILE *inf);
|
||||
void md_tstpsignal();
|
||||
void md_tstphold();
|
||||
void md_tstpresume();
|
||||
void md_ignoreallsignals();
|
||||
void md_onsignal_autosave();
|
||||
void md_onsignal_exit();
|
||||
void md_onsignal_default();
|
||||
int md_issymlink(char *sp);
|
||||
|
||||
686
fight.c
Normal file
686
fight.c
Normal file
@@ -0,0 +1,686 @@
|
||||
/*
|
||||
* All the fighting gets done here
|
||||
*
|
||||
* @(#)fight.c 4.67 (Berkeley) 09/06/83
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <curses.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include "rogue.h"
|
||||
|
||||
#define EQSTR(a, b) (strcmp(a, b) == 0)
|
||||
|
||||
char *h_names[] = { /* strings for hitting */
|
||||
" scored an excellent hit on ",
|
||||
" hit ",
|
||||
" have injured ",
|
||||
" swing and hit ",
|
||||
" scored an excellent hit on ",
|
||||
" hit ",
|
||||
" has injured ",
|
||||
" swings and hits "
|
||||
};
|
||||
|
||||
char *m_names[] = { /* strings for missing */
|
||||
" miss",
|
||||
" swing and miss",
|
||||
" barely miss",
|
||||
" don't hit",
|
||||
" misses",
|
||||
" swings and misses",
|
||||
" barely misses",
|
||||
" doesn't hit",
|
||||
};
|
||||
|
||||
/*
|
||||
* adjustments to hit probabilities due to strength
|
||||
*/
|
||||
static int str_plus[] = {
|
||||
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
|
||||
};
|
||||
|
||||
/*
|
||||
* adjustments to damage done due to strength
|
||||
*/
|
||||
static int add_dam[] = {
|
||||
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3,
|
||||
3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6
|
||||
};
|
||||
|
||||
/*
|
||||
* fight:
|
||||
* The player attacks the monster.
|
||||
*/
|
||||
int
|
||||
fight(coord *mp, THING *weap, bool thrown)
|
||||
{
|
||||
register THING *tp;
|
||||
register bool did_hit = TRUE;
|
||||
register char *mname, ch;
|
||||
|
||||
/*
|
||||
* Find the monster we want to fight
|
||||
*/
|
||||
#ifdef MASTER
|
||||
if ((tp = moat(mp->y, mp->x)) == NULL)
|
||||
debug("Fight what @ %d,%d", mp->y, mp->x);
|
||||
#else
|
||||
tp = moat(mp->y, mp->x);
|
||||
#endif
|
||||
/*
|
||||
* Since we are fighting, things are not quiet so no healing takes
|
||||
* place.
|
||||
*/
|
||||
count = 0;
|
||||
quiet = 0;
|
||||
runto(mp);
|
||||
/*
|
||||
* Let him know it was really a xeroc (if it was one).
|
||||
*/
|
||||
ch = '\0';
|
||||
if (tp->t_type == 'X' && tp->t_disguise != 'X' && !on(player, ISBLIND))
|
||||
{
|
||||
tp->t_disguise = 'X';
|
||||
if (on(player, ISHALU)) {
|
||||
ch = (char)(rnd(26) + 'A');
|
||||
mvaddch(tp->t_pos.y, tp->t_pos.x, ch);
|
||||
}
|
||||
msg(choose_str("heavy! That's a nasty critter!",
|
||||
"wait! That's a xeroc!"));
|
||||
if (!thrown)
|
||||
return FALSE;
|
||||
}
|
||||
mname = set_mname(tp);
|
||||
did_hit = FALSE;
|
||||
has_hit = (terse && !to_death);
|
||||
if (roll_em(&player, tp, weap, thrown))
|
||||
{
|
||||
did_hit = FALSE;
|
||||
if (thrown)
|
||||
thunk(weap, mname, terse);
|
||||
else
|
||||
hit((char *) NULL, mname, terse);
|
||||
if (on(player, CANHUH))
|
||||
{
|
||||
did_hit = TRUE;
|
||||
tp->t_flags |= ISHUH;
|
||||
player.t_flags &= ~CANHUH;
|
||||
endmsg();
|
||||
has_hit = FALSE;
|
||||
msg("your hands stop glowing %s", pick_color("red"));
|
||||
}
|
||||
if (tp->t_stats.s_hpt <= 0)
|
||||
killed(tp, TRUE);
|
||||
else if (did_hit && !on(player, ISBLIND))
|
||||
msg("%s appears confused", mname);
|
||||
did_hit = TRUE;
|
||||
}
|
||||
else
|
||||
if (thrown)
|
||||
bounce(weap, mname, terse);
|
||||
else
|
||||
miss((char *) NULL, mname, terse);
|
||||
return did_hit;
|
||||
}
|
||||
|
||||
/*
|
||||
* attack:
|
||||
* The monster attacks the player
|
||||
*/
|
||||
int
|
||||
attack(THING *mp)
|
||||
{
|
||||
register char *mname;
|
||||
register int oldhp;
|
||||
|
||||
/*
|
||||
* Since this is an attack, stop running and any healing that was
|
||||
* going on at the time.
|
||||
*/
|
||||
running = FALSE;
|
||||
count = 0;
|
||||
quiet = 0;
|
||||
if (to_death && !on(*mp, ISTARGET))
|
||||
{
|
||||
to_death = FALSE;
|
||||
kamikaze = FALSE;
|
||||
}
|
||||
if (mp->t_type == 'X' && mp->t_disguise != 'X' && !on(player, ISBLIND))
|
||||
{
|
||||
mp->t_disguise = 'X';
|
||||
if (on(player, ISHALU))
|
||||
mvaddch(mp->t_pos.y, mp->t_pos.x, rnd(26) + 'A');
|
||||
}
|
||||
mname = set_mname(mp);
|
||||
oldhp = pstats.s_hpt;
|
||||
if (roll_em(mp, &player, (THING *) NULL, FALSE))
|
||||
{
|
||||
if (mp->t_type != 'I')
|
||||
{
|
||||
if (has_hit)
|
||||
addmsg(". ");
|
||||
hit(mname, (char *) NULL, FALSE);
|
||||
}
|
||||
else
|
||||
if (has_hit)
|
||||
endmsg();
|
||||
has_hit = FALSE;
|
||||
if (pstats.s_hpt <= 0)
|
||||
death(mp->t_type); /* Bye bye life ... */
|
||||
else if (!kamikaze)
|
||||
{
|
||||
oldhp -= pstats.s_hpt;
|
||||
if (oldhp > max_hit)
|
||||
max_hit = oldhp;
|
||||
if (pstats.s_hpt <= max_hit)
|
||||
to_death = FALSE;
|
||||
}
|
||||
if (!on(*mp, ISCANC))
|
||||
switch (mp->t_type)
|
||||
{
|
||||
case 'A':
|
||||
/*
|
||||
* If an aquator hits, you can lose armor class.
|
||||
*/
|
||||
rust_armor(cur_armor);
|
||||
when 'I':
|
||||
/*
|
||||
* The ice monster freezes you
|
||||
*/
|
||||
player.t_flags &= ~ISRUN;
|
||||
if (!no_command)
|
||||
{
|
||||
addmsg("you are frozen");
|
||||
if (!terse)
|
||||
addmsg(" by the %s", mname);
|
||||
endmsg();
|
||||
}
|
||||
no_command += rnd(2) + 2;
|
||||
if (no_command > BORE_LEVEL)
|
||||
death('h');
|
||||
when 'R':
|
||||
/*
|
||||
* Rattlesnakes have poisonous bites
|
||||
*/
|
||||
if (!save(VS_POISON))
|
||||
{
|
||||
if (!ISWEARING(R_SUSTSTR))
|
||||
{
|
||||
chg_str(-1);
|
||||
if (!terse)
|
||||
msg("you feel a bite in your leg and now feel weaker");
|
||||
else
|
||||
msg("a bite has weakened you");
|
||||
}
|
||||
else if (!to_death)
|
||||
{
|
||||
if (!terse)
|
||||
msg("a bite momentarily weakens you");
|
||||
else
|
||||
msg("bite has no effect");
|
||||
}
|
||||
}
|
||||
when 'W':
|
||||
case 'V':
|
||||
/*
|
||||
* Wraiths might drain energy levels, and Vampires
|
||||
* can steal max_hp
|
||||
*/
|
||||
if (rnd(100) < (mp->t_type == 'W' ? 15 : 30))
|
||||
{
|
||||
register int fewer;
|
||||
|
||||
if (mp->t_type == 'W')
|
||||
{
|
||||
if (pstats.s_exp == 0)
|
||||
death('W'); /* All levels gone */
|
||||
if (--pstats.s_lvl == 0)
|
||||
{
|
||||
pstats.s_exp = 0;
|
||||
pstats.s_lvl = 1;
|
||||
}
|
||||
else
|
||||
pstats.s_exp = e_levels[pstats.s_lvl-1]+1;
|
||||
fewer = roll(1, 10);
|
||||
}
|
||||
else
|
||||
fewer = roll(1, 3);
|
||||
pstats.s_hpt -= fewer;
|
||||
max_hp -= fewer;
|
||||
if (pstats.s_hpt <= 0)
|
||||
pstats.s_hpt = 1;
|
||||
if (max_hp <= 0)
|
||||
death(mp->t_type);
|
||||
msg("you suddenly feel weaker");
|
||||
}
|
||||
when 'F':
|
||||
/*
|
||||
* Venus Flytrap stops the poor guy from moving
|
||||
*/
|
||||
player.t_flags |= ISHELD;
|
||||
sprintf(monsters['F'-'A'].m_stats.s_dmg,"%dx1", ++vf_hit);
|
||||
if (--pstats.s_hpt <= 0)
|
||||
death('F');
|
||||
when 'L':
|
||||
{
|
||||
/*
|
||||
* Leperachaun steals some gold
|
||||
*/
|
||||
register int lastpurse;
|
||||
|
||||
lastpurse = purse;
|
||||
purse -= GOLDCALC;
|
||||
if (!save(VS_MAGIC))
|
||||
purse -= GOLDCALC + GOLDCALC + GOLDCALC + GOLDCALC;
|
||||
if (purse < 0)
|
||||
purse = 0;
|
||||
remove_mon(&mp->t_pos, mp, FALSE);
|
||||
mp=NULL;
|
||||
if (purse != lastpurse)
|
||||
msg("your purse feels lighter");
|
||||
}
|
||||
when 'N':
|
||||
{
|
||||
register THING *obj, *steal;
|
||||
register int nobj;
|
||||
|
||||
/*
|
||||
* Nymph's steal a magic item, look through the pack
|
||||
* and pick out one we like.
|
||||
*/
|
||||
steal = NULL;
|
||||
for (nobj = 0, obj = pack; obj != NULL; obj = next(obj))
|
||||
if (obj != cur_armor && obj != cur_weapon
|
||||
&& obj != cur_ring[LEFT] && obj != cur_ring[RIGHT]
|
||||
&& is_magic(obj) && rnd(++nobj) == 0)
|
||||
steal = obj;
|
||||
if (steal != NULL)
|
||||
{
|
||||
remove_mon(&mp->t_pos, moat(mp->t_pos.y, mp->t_pos.x), FALSE);
|
||||
mp=NULL;
|
||||
leave_pack(steal, FALSE, FALSE);
|
||||
msg("she stole %s!", inv_name(steal, TRUE));
|
||||
discard(steal);
|
||||
}
|
||||
}
|
||||
otherwise:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (mp->t_type != 'I')
|
||||
{
|
||||
if (has_hit)
|
||||
{
|
||||
addmsg(". ");
|
||||
has_hit = FALSE;
|
||||
}
|
||||
if (mp->t_type == 'F')
|
||||
{
|
||||
pstats.s_hpt -= vf_hit;
|
||||
if (pstats.s_hpt <= 0)
|
||||
death(mp->t_type); /* Bye bye life ... */
|
||||
}
|
||||
miss(mname, (char *) NULL, FALSE);
|
||||
}
|
||||
if (fight_flush && !to_death)
|
||||
flush_type();
|
||||
count = 0;
|
||||
status();
|
||||
if (mp == NULL)
|
||||
return(-1);
|
||||
else
|
||||
return(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* set_mname:
|
||||
* return the monster name for the given monster
|
||||
*/
|
||||
char *
|
||||
set_mname(THING *tp)
|
||||
{
|
||||
int ch;
|
||||
char *mname;
|
||||
static char tbuf[MAXSTR] = { 't', 'h', 'e', ' ' };
|
||||
|
||||
if (!see_monst(tp) && !on(player, SEEMONST))
|
||||
return (terse ? "it" : "something");
|
||||
else if (on(player, ISHALU))
|
||||
{
|
||||
move(tp->t_pos.y, tp->t_pos.x);
|
||||
ch = toascii(inch());
|
||||
if (!isupper(ch))
|
||||
ch = rnd(26);
|
||||
else
|
||||
ch -= 'A';
|
||||
mname = monsters[ch].m_name;
|
||||
}
|
||||
else
|
||||
mname = monsters[tp->t_type - 'A'].m_name;
|
||||
strcpy(&tbuf[4], mname);
|
||||
return tbuf;
|
||||
}
|
||||
|
||||
/*
|
||||
* swing:
|
||||
* Returns true if the swing hits
|
||||
*/
|
||||
int
|
||||
swing(int at_lvl, int op_arm, int wplus)
|
||||
{
|
||||
int res = rnd(20);
|
||||
int need = (20 - at_lvl) - op_arm;
|
||||
|
||||
return (res + wplus >= need);
|
||||
}
|
||||
|
||||
/*
|
||||
* roll_em:
|
||||
* Roll several attacks
|
||||
*/
|
||||
bool
|
||||
roll_em(THING *thatt, THING *thdef, THING *weap, bool hurl)
|
||||
{
|
||||
register struct stats *att, *def;
|
||||
register char *cp;
|
||||
register int ndice, nsides, def_arm;
|
||||
register bool did_hit = FALSE;
|
||||
register int hplus;
|
||||
register int dplus;
|
||||
register int damage;
|
||||
|
||||
att = &thatt->t_stats;
|
||||
def = &thdef->t_stats;
|
||||
if (weap == NULL)
|
||||
{
|
||||
cp = att->s_dmg;
|
||||
dplus = 0;
|
||||
hplus = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
hplus = (weap == NULL ? 0 : weap->o_hplus);
|
||||
dplus = (weap == NULL ? 0 : weap->o_dplus);
|
||||
if (weap == cur_weapon)
|
||||
{
|
||||
if (ISRING(LEFT, R_ADDDAM))
|
||||
dplus += cur_ring[LEFT]->o_arm;
|
||||
else if (ISRING(LEFT, R_ADDHIT))
|
||||
hplus += cur_ring[LEFT]->o_arm;
|
||||
if (ISRING(RIGHT, R_ADDDAM))
|
||||
dplus += cur_ring[RIGHT]->o_arm;
|
||||
else if (ISRING(RIGHT, R_ADDHIT))
|
||||
hplus += cur_ring[RIGHT]->o_arm;
|
||||
}
|
||||
cp = weap->o_damage;
|
||||
if (hurl)
|
||||
{
|
||||
if ((weap->o_flags&ISMISL) && cur_weapon != NULL &&
|
||||
cur_weapon->o_which == weap->o_launch)
|
||||
{
|
||||
cp = weap->o_hurldmg;
|
||||
hplus += cur_weapon->o_hplus;
|
||||
dplus += cur_weapon->o_dplus;
|
||||
}
|
||||
else if (weap->o_launch < 0)
|
||||
cp = weap->o_hurldmg;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* If the creature being attacked is not running (alseep or held)
|
||||
* then the attacker gets a plus four bonus to hit.
|
||||
*/
|
||||
if (!on(*thdef, ISRUN))
|
||||
hplus += 4;
|
||||
def_arm = def->s_arm;
|
||||
if (def == &pstats)
|
||||
{
|
||||
if (cur_armor != NULL)
|
||||
def_arm = cur_armor->o_arm;
|
||||
if (ISRING(LEFT, R_PROTECT))
|
||||
def_arm -= cur_ring[LEFT]->o_arm;
|
||||
if (ISRING(RIGHT, R_PROTECT))
|
||||
def_arm -= cur_ring[RIGHT]->o_arm;
|
||||
}
|
||||
while(cp != NULL && *cp != '\0')
|
||||
{
|
||||
ndice = atoi(cp);
|
||||
if ((cp = strchr(cp, 'x')) == NULL)
|
||||
break;
|
||||
nsides = atoi(++cp);
|
||||
if (swing(att->s_lvl, def_arm, hplus + str_plus[att->s_str]))
|
||||
{
|
||||
int proll;
|
||||
|
||||
proll = roll(ndice, nsides);
|
||||
#ifdef MASTER
|
||||
if (ndice + nsides > 0 && proll <= 0)
|
||||
debug("Damage for %dx%d came out %d, dplus = %d, add_dam = %d, def_arm = %d", ndice, nsides, proll, dplus, add_dam[att->s_str], def_arm);
|
||||
#endif
|
||||
damage = dplus + proll + add_dam[att->s_str];
|
||||
def->s_hpt -= max(0, damage);
|
||||
did_hit = TRUE;
|
||||
}
|
||||
if ((cp = strchr(cp, '/')) == NULL)
|
||||
break;
|
||||
cp++;
|
||||
}
|
||||
return did_hit;
|
||||
}
|
||||
|
||||
/*
|
||||
* prname:
|
||||
* The print name of a combatant
|
||||
*/
|
||||
char *
|
||||
prname(char *mname, bool upper)
|
||||
{
|
||||
static char tbuf[MAXSTR];
|
||||
|
||||
*tbuf = '\0';
|
||||
if (mname == 0)
|
||||
strcpy(tbuf, "you");
|
||||
else
|
||||
strcpy(tbuf, mname);
|
||||
if (upper)
|
||||
*tbuf = (char) toupper(*tbuf);
|
||||
return tbuf;
|
||||
}
|
||||
|
||||
/*
|
||||
* thunk:
|
||||
* A missile hits a monster
|
||||
*/
|
||||
void
|
||||
thunk(THING *weap, char *mname, bool noend)
|
||||
{
|
||||
if (to_death)
|
||||
return;
|
||||
if (weap->o_type == WEAPON)
|
||||
addmsg("the %s hits ", weap_info[weap->o_which].oi_name);
|
||||
else
|
||||
addmsg("you hit ");
|
||||
addmsg("%s", mname);
|
||||
if (!noend)
|
||||
endmsg();
|
||||
}
|
||||
|
||||
/*
|
||||
* hit:
|
||||
* Print a message to indicate a succesful hit
|
||||
*/
|
||||
|
||||
void
|
||||
hit(char *er, char *ee, bool noend)
|
||||
{
|
||||
int i;
|
||||
char *s;
|
||||
extern char *h_names[];
|
||||
|
||||
if (to_death)
|
||||
return;
|
||||
addmsg(prname(er, TRUE));
|
||||
if (terse)
|
||||
s = " hit";
|
||||
else
|
||||
{
|
||||
i = rnd(4);
|
||||
if (er != NULL)
|
||||
i += 4;
|
||||
s = h_names[i];
|
||||
}
|
||||
addmsg(s);
|
||||
if (!terse)
|
||||
addmsg(prname(ee, FALSE));
|
||||
if (!noend)
|
||||
endmsg();
|
||||
}
|
||||
|
||||
/*
|
||||
* miss:
|
||||
* Print a message to indicate a poor swing
|
||||
*/
|
||||
void
|
||||
miss(char *er, char *ee, bool noend)
|
||||
{
|
||||
int i;
|
||||
extern char *m_names[];
|
||||
|
||||
if (to_death)
|
||||
return;
|
||||
addmsg(prname(er, TRUE));
|
||||
if (terse)
|
||||
i = 0;
|
||||
else
|
||||
i = rnd(4);
|
||||
if (er != NULL)
|
||||
i += 4;
|
||||
addmsg(m_names[i]);
|
||||
if (!terse)
|
||||
addmsg(" %s", prname(ee, FALSE));
|
||||
if (!noend)
|
||||
endmsg();
|
||||
}
|
||||
|
||||
/*
|
||||
* bounce:
|
||||
* A missile misses a monster
|
||||
*/
|
||||
void
|
||||
bounce(THING *weap, char *mname, bool noend)
|
||||
{
|
||||
if (to_death)
|
||||
return;
|
||||
if (weap->o_type == WEAPON)
|
||||
addmsg("the %s misses ", weap_info[weap->o_which].oi_name);
|
||||
else
|
||||
addmsg("you missed ");
|
||||
addmsg(mname);
|
||||
if (!noend)
|
||||
endmsg();
|
||||
}
|
||||
|
||||
/*
|
||||
* remove_mon:
|
||||
* Remove a monster from the screen
|
||||
*/
|
||||
void
|
||||
remove_mon(coord *mp, THING *tp, bool waskill)
|
||||
{
|
||||
register THING *obj, *nexti;
|
||||
|
||||
for (obj = tp->t_pack; obj != NULL; obj = nexti)
|
||||
{
|
||||
nexti = next(obj);
|
||||
obj->o_pos = tp->t_pos;
|
||||
detach(tp->t_pack, obj);
|
||||
if (waskill)
|
||||
fall(obj, FALSE);
|
||||
else
|
||||
discard(obj);
|
||||
}
|
||||
moat(mp->y, mp->x) = NULL;
|
||||
mvaddch(mp->y, mp->x, tp->t_oldch);
|
||||
detach(mlist, tp);
|
||||
if (on(*tp, ISTARGET))
|
||||
{
|
||||
kamikaze = FALSE;
|
||||
to_death = FALSE;
|
||||
if (fight_flush)
|
||||
flush_type();
|
||||
}
|
||||
discard(tp);
|
||||
}
|
||||
|
||||
/*
|
||||
* killed:
|
||||
* Called to put a monster to death
|
||||
*/
|
||||
void
|
||||
killed(THING *tp, bool pr)
|
||||
{
|
||||
char *mname;
|
||||
|
||||
pstats.s_exp += tp->t_stats.s_exp;
|
||||
|
||||
/*
|
||||
* If the monster was a venus flytrap, un-hold him
|
||||
*/
|
||||
switch (tp->t_type)
|
||||
{
|
||||
case 'F':
|
||||
player.t_flags &= ~ISHELD;
|
||||
vf_hit = 0;
|
||||
strcpy(monsters['F'-'A'].m_stats.s_dmg, "000x0");
|
||||
when 'L':
|
||||
{
|
||||
THING *gold;
|
||||
|
||||
if (fallpos(&tp->t_pos, &tp->t_room->r_gold) && level >= max_level)
|
||||
{
|
||||
gold = new_item();
|
||||
gold->o_type = GOLD;
|
||||
gold->o_goldval = GOLDCALC;
|
||||
if (save(VS_MAGIC))
|
||||
gold->o_goldval += GOLDCALC + GOLDCALC
|
||||
+ GOLDCALC + GOLDCALC;
|
||||
attach(tp->t_pack, gold);
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Get rid of the monster.
|
||||
*/
|
||||
mname = set_mname(tp);
|
||||
remove_mon(&tp->t_pos, tp, TRUE);
|
||||
if (pr)
|
||||
{
|
||||
if (has_hit)
|
||||
{
|
||||
addmsg(". Defeated ");
|
||||
has_hit = FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("you have ");
|
||||
addmsg("defeated ");
|
||||
}
|
||||
msg(mname);
|
||||
}
|
||||
/*
|
||||
* Do adjustments if he went up a level
|
||||
*/
|
||||
check_level();
|
||||
if (fight_flush)
|
||||
flush_type();
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package game
|
||||
|
||||
// armor.c — misc functions for dealing with armor.
|
||||
|
||||
// wear lets the player put armor on (armor.c wear).
|
||||
func (g *RogueGame) wear() {
|
||||
p := &g.Player
|
||||
|
||||
obj, ok := g.promptPackItem("wear", KindArmor)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if p.CurArmor != nil {
|
||||
g.addmsgf("you are already wearing some")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(". You'll have to take it off first")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
g.After = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindArmor {
|
||||
g.msg("you can't wear that")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.wasteTime()
|
||||
obj.Flags.Set(Known)
|
||||
sp := g.inventoryName(obj, true)
|
||||
p.CurArmor = obj
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you are now ")
|
||||
}
|
||||
|
||||
g.msg("wearing %s", sp)
|
||||
}
|
||||
|
||||
// takeOff gets the armor off of the player's back (armor.c take_off).
|
||||
func (g *RogueGame) takeOff() {
|
||||
p := &g.Player
|
||||
|
||||
obj := p.CurArmor
|
||||
if obj == nil {
|
||||
g.After = false
|
||||
if g.Options.Terse {
|
||||
g.msg("not wearing armor")
|
||||
} else {
|
||||
g.msg("you aren't wearing any armor")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !g.dropCheck(p.CurArmor) {
|
||||
return
|
||||
}
|
||||
|
||||
p.CurArmor = nil
|
||||
|
||||
if g.Options.Terse {
|
||||
g.addmsgf("was")
|
||||
} else {
|
||||
g.addmsgf("you used to be")
|
||||
}
|
||||
|
||||
g.msg(" wearing %c) %s", obj.PackCh, g.inventoryName(obj, true))
|
||||
}
|
||||
|
||||
// wasteTime does nothing but let other things happen (armor.c waste_time).
|
||||
func (g *RogueGame) wasteTime() {
|
||||
g.DoDaemons(Before)
|
||||
g.DoFuses(Before)
|
||||
g.DoDaemons(After)
|
||||
g.DoFuses(After)
|
||||
}
|
||||
@@ -1,515 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// Tests for the signal-triggered autosave handoff (issue #24): the signal
|
||||
// goroutine must never encode game state itself, and the game goroutine
|
||||
// must answer wherever it is parked.
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// autoSaveWait is the deadline the tests hand AutoSaveOnSignal when they
|
||||
// expect the save to be taken. It is long enough that a loaded machine
|
||||
// cannot turn a working handoff into a spurious failure, and it is never
|
||||
// actually waited out on a passing run.
|
||||
const autoSaveWait = 10 * time.Second
|
||||
|
||||
// TestAutoSaveOnSignalRacesTurnLoop is the test issue #24 exists for: it
|
||||
// drives the real turn loop on one goroutine while another asks for a
|
||||
// signal-triggered autosave over and over, which is the interleaving no
|
||||
// test in the suite used to produce. `make test` runs with -race, so a
|
||||
// save that encodes the live game tree from the asking goroutine — what
|
||||
// the old AutoSave did straight from the signal handler — is reported as
|
||||
// a data race and fails this test.
|
||||
//
|
||||
// Non-vacuity: with AutoSaveOnSignal's body replaced by a direct
|
||||
// g.autoSave() call, i.e. exactly the pre-#24 behavior, this test fails
|
||||
// under -race with the encoder reading state that command() is writing.
|
||||
func TestAutoSaveOnSignalRacesTurnLoop(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Same mix as TestTurnLoopCrashSweep: the spaces answer any --More--
|
||||
// prompt, and the script is long enough that the drive never runs it
|
||||
// out.
|
||||
script := []byte(strings.Repeat("h j k l y u b n s . ", 400))
|
||||
|
||||
g := New(Params{Seed: 20260809, Term: &testTerm{input: script}})
|
||||
g.FileName = filepath.Join(t.TempDir(), "rogue.save")
|
||||
g.startLevel()
|
||||
g.prePlay()
|
||||
|
||||
const wantSaves = 25
|
||||
|
||||
var taken int
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
for range wantSaves {
|
||||
if g.AutoSaveOnSignal(autoSaveWait) {
|
||||
taken++
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
driveUntilDone(t, g, done)
|
||||
|
||||
// The close of done orders that goroutine's writes before this read.
|
||||
if taken != wantSaves {
|
||||
t.Errorf("saves taken = %d, want %d", taken, wantSaves)
|
||||
}
|
||||
|
||||
// Every request was answered by the turn loop, so the file is the
|
||||
// work of the game goroutine and must be a whole save.
|
||||
assertRestorable(t, g.FileName)
|
||||
}
|
||||
|
||||
// driveUntilDone runs turns until the saving goroutine is finished,
|
||||
// fortifying the hero each turn so no death exits the test binary. The
|
||||
// turn cap keeps a broken handoff from hanging the suite instead of
|
||||
// failing it.
|
||||
func driveUntilDone(t *testing.T, g *RogueGame, done <-chan struct{}) {
|
||||
t.Helper()
|
||||
|
||||
const maxTurns = 1000
|
||||
|
||||
for range maxTurns {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
fortify(g)
|
||||
g.command()
|
||||
}
|
||||
|
||||
t.Fatal("the turn loop ran out of turns before the saves were taken")
|
||||
}
|
||||
|
||||
// TestAutoSaveOnSignalWhileBlockedOnInput is the case the fix is really
|
||||
// for: the connection drops while the player is staring at the screen,
|
||||
// so the game goroutine is parked in ReadChar and will not reach the
|
||||
// between-turns check on its own. A flag checked only between turns would
|
||||
// never be looked at here.
|
||||
func TestAutoSaveOnSignalWhileBlockedOnInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
bt := newBlockingTerm()
|
||||
g := mkBlockedGame(t, bt)
|
||||
|
||||
read := make(chan byte)
|
||||
|
||||
go func() { read <- g.readchar() }()
|
||||
|
||||
// The wake is buffered, so this is correct whether or not the reader
|
||||
// has reached ReadChar yet.
|
||||
if !g.AutoSaveOnSignal(autoSaveWait) {
|
||||
t.Fatal("the save was not taken while the game was blocked on input")
|
||||
}
|
||||
|
||||
assertRestorable(t, g.FileName)
|
||||
|
||||
// The interrupt must not have been mistaken for a keystroke: the
|
||||
// reader is still waiting, and still returns the real key.
|
||||
bt.keys <- 'x'
|
||||
|
||||
if ch := <-read; ch != 'x' {
|
||||
t.Errorf("readchar() = %q, want 'x'", ch)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoSaveOnSignalWhileInShellEscape covers the other place the game
|
||||
// goroutine parks for an unbounded time: the `!` shell escape, where it
|
||||
// used to sit inside the shell call with no way to answer. A dropped line
|
||||
// while the player is off in a shell is as much a hangup as any other.
|
||||
func TestAutoSaveOnSignalWhileInShellEscape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := &shellTerm{
|
||||
blockingTerm: newBlockingTerm(),
|
||||
entered: make(chan struct{}),
|
||||
release: make(chan struct{}),
|
||||
}
|
||||
g := mkBlockedGame(t, st)
|
||||
|
||||
left := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(left)
|
||||
|
||||
g.shell()
|
||||
}()
|
||||
|
||||
<-st.entered
|
||||
|
||||
if !g.AutoSaveOnSignal(autoSaveWait) {
|
||||
t.Fatal("the save was not taken while the game was in the shell escape")
|
||||
}
|
||||
|
||||
assertRestorable(t, g.FileName)
|
||||
|
||||
close(st.release)
|
||||
<-left
|
||||
}
|
||||
|
||||
// TestShellEscapePanicUnwindsTheGameGoroutine pins the reason
|
||||
// runShellEscape recovers its helper's panic.
|
||||
//
|
||||
// term.Tcell.ShellEscape panics when Screen.Resume fails, and the shell
|
||||
// now runs on a helper goroutine. A panic reaching the top of that helper
|
||||
// would kill the process without running the deferred calls of any other
|
||||
// goroutine — including cmd/rogue/main.go's `defer t.Fini()`, which is
|
||||
// the only thing that takes the tty back out of raw mode. That is issue
|
||||
// #12's failure, and it would land on the one path where the terminal is
|
||||
// already broken.
|
||||
//
|
||||
// So the panic has to arrive on the goroutine that runs the game, with
|
||||
// that goroutine's deferred restore still on the stack. This test stands
|
||||
// in for main: a Fini deferred around the g.shell() call, and the panic
|
||||
// caught after it, asserting both that the restore ran and that the
|
||||
// original value came through. Against the unrecovered version there is
|
||||
// nothing to assert — the panic escapes a helper goroutine and takes the
|
||||
// whole test binary down, which is the failure being prevented.
|
||||
func TestShellEscapePanicUnwindsTheGameGoroutine(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pt := &panickingShellTerm{blockingTerm: newBlockingTerm()}
|
||||
g := mkBlockedGame(t, pt)
|
||||
|
||||
caught := make(chan any, 1)
|
||||
|
||||
go func() {
|
||||
// Registered first, so it runs last: it sees the terminal
|
||||
// already restored, exactly as the runtime would have printed
|
||||
// the trace after main's Fini.
|
||||
defer func() { caught <- recover() }()
|
||||
|
||||
// Stands in for cmd/rogue/main.go's `defer t.Fini()`.
|
||||
defer pt.Fini()
|
||||
|
||||
g.shell()
|
||||
}()
|
||||
|
||||
got := <-caught
|
||||
|
||||
if got == nil {
|
||||
t.Fatal("the resume failure did not reach the game goroutine")
|
||||
}
|
||||
|
||||
if msg, ok := got.(string); !ok || msg != errShellResume {
|
||||
t.Errorf("recovered %v, want %q", got, errShellResume)
|
||||
}
|
||||
|
||||
if !pt.restored {
|
||||
t.Error("the terminal was not restored on the way out")
|
||||
}
|
||||
|
||||
// shell() must not have resumed into its InShell reset and refresh:
|
||||
// there is no screen left to draw into.
|
||||
if !g.InShell {
|
||||
t.Error("shell() carried on drawing after the resume failed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoSaveOnSignalTimesOutLeavingTheOldSave pins the backstop: a game
|
||||
// goroutine that never reaches a service point must not hold the process
|
||||
// open, and giving up must cost the player nothing. The old save is still
|
||||
// there, byte for byte — which is the whole point of renaming over the
|
||||
// target instead of removing it first.
|
||||
func TestAutoSaveOnSignalTimesOutLeavingTheOldSave(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 77)
|
||||
g.FileName = filepath.Join(t.TempDir(), "rogue.save")
|
||||
|
||||
const old = "an older save nobody is allowed to destroy"
|
||||
|
||||
writeErr := os.WriteFile(g.FileName, []byte(old), 0o600)
|
||||
if writeErr != nil {
|
||||
t.Fatal(writeErr)
|
||||
}
|
||||
|
||||
// Nothing drives the turn loop, so nothing will ever answer.
|
||||
start := time.Now()
|
||||
|
||||
if g.AutoSaveOnSignal(100 * time.Millisecond) {
|
||||
t.Error("AutoSaveOnSignal reported a save that nobody took")
|
||||
}
|
||||
|
||||
if waited := time.Since(start); waited > time.Second {
|
||||
t.Errorf("waited %v for an unanswered save, want the deadline to bound it",
|
||||
waited)
|
||||
}
|
||||
|
||||
got, readErr := os.ReadFile(g.FileName)
|
||||
if readErr != nil {
|
||||
t.Fatalf("the previous save was destroyed: %v", readErr)
|
||||
}
|
||||
|
||||
if string(got) != old {
|
||||
t.Error("the previous save was overwritten by a save that never ran")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoSaveOnSignalWithoutASaveFile covers the death demo's terminal
|
||||
// case: a game with no file name has nothing to write, and must say so
|
||||
// rather than reporting a save that did not happen.
|
||||
func TestAutoSaveOnSignalWithoutASaveFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := New(Params{Seed: 5, Term: &testTerm{
|
||||
input: []byte(strings.Repeat("s . ", 200)),
|
||||
}})
|
||||
g.FileName = ""
|
||||
g.startLevel()
|
||||
g.prePlay()
|
||||
|
||||
var answered bool
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
answered = g.AutoSaveOnSignal(autoSaveWait)
|
||||
}()
|
||||
|
||||
driveUntilDone(t, g, done)
|
||||
|
||||
if answered {
|
||||
t.Error("AutoSaveOnSignal = true with no save file name")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSaveFileReplacesTargetAtomically pins the write discipline: the new
|
||||
// save arrives by rename, so the file the player already had is never
|
||||
// written into, and the temporary file it came from is not left lying in
|
||||
// the save directory.
|
||||
//
|
||||
// The load-bearing assertion is the handle opened before the save. A
|
||||
// rename leaves the old file whole and merely stops it being reachable by
|
||||
// name, so that handle still reads the old save; the truncate-in-place
|
||||
// write this replaced would empty it under the reader — the same
|
||||
// in-place write that, interrupted, left the player with a file that
|
||||
// could no longer be restored.
|
||||
func TestSaveFileReplacesTargetAtomically(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 11)
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "rogue.save")
|
||||
|
||||
const old = "an older save"
|
||||
|
||||
writeErr := os.WriteFile(path, []byte(old), 0o600)
|
||||
if writeErr != nil {
|
||||
t.Fatal(writeErr)
|
||||
}
|
||||
|
||||
held, openErr := os.Open(path) //nolint:gosec // G304: test temp path
|
||||
if openErr != nil {
|
||||
t.Fatal(openErr)
|
||||
}
|
||||
|
||||
defer func() { _ = held.Close() }()
|
||||
|
||||
saveErr := g.saveFile(path)
|
||||
if saveErr != nil {
|
||||
t.Fatalf("saveFile: %v", saveErr)
|
||||
}
|
||||
|
||||
kept, readErr := io.ReadAll(held)
|
||||
if readErr != nil {
|
||||
t.Fatalf("reading the file that was there before the save: %v", readErr)
|
||||
}
|
||||
|
||||
if string(kept) != old {
|
||||
t.Errorf("the previous save was written into rather than replaced: %q",
|
||||
string(kept))
|
||||
}
|
||||
|
||||
entries, readErr := os.ReadDir(dir)
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
|
||||
if len(entries) != 1 || entries[0].Name() != "rogue.save" {
|
||||
t.Errorf("save directory = %v, want just the save file", names(entries))
|
||||
}
|
||||
|
||||
info, statErr := os.Stat(path)
|
||||
if statErr != nil {
|
||||
t.Fatal(statErr)
|
||||
}
|
||||
|
||||
if perm := info.Mode().Perm(); perm != 0o400 {
|
||||
t.Errorf("save file mode = %v, want 0400", perm)
|
||||
}
|
||||
|
||||
assertRestorable(t, path)
|
||||
}
|
||||
|
||||
// TestSaveFileLeavesTargetWhenTheRenameFails is the other half of the
|
||||
// same discipline: a save that cannot be completed must leave what the
|
||||
// player already had. The target here is a non-empty directory, which no
|
||||
// rename can replace — the one write failure that can be forced without
|
||||
// depending on file permissions, and therefore on not being root.
|
||||
func TestSaveFileLeavesTargetWhenTheRenameFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 12)
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "rogue.save")
|
||||
|
||||
mkErr := os.Mkdir(path, 0o700)
|
||||
if mkErr != nil {
|
||||
t.Fatal(mkErr)
|
||||
}
|
||||
|
||||
keep := filepath.Join(path, "keep")
|
||||
|
||||
writeErr := os.WriteFile(keep, []byte("still here"), 0o600)
|
||||
if writeErr != nil {
|
||||
t.Fatal(writeErr)
|
||||
}
|
||||
|
||||
saveErr := g.saveFile(path)
|
||||
if saveErr == nil {
|
||||
t.Error("saveFile over an unreplaceable target reported success")
|
||||
}
|
||||
|
||||
_, statErr := os.Stat(keep)
|
||||
if statErr != nil {
|
||||
t.Errorf("the target was damaged by a failed save: %v", statErr)
|
||||
}
|
||||
|
||||
entries, readErr := os.ReadDir(dir)
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
|
||||
if len(entries) != 1 {
|
||||
t.Errorf("save directory = %v, want no temporary file left behind",
|
||||
names(entries))
|
||||
}
|
||||
}
|
||||
|
||||
// names lists directory entry names for a failure message.
|
||||
func names(entries []os.DirEntry) []string {
|
||||
out := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
out = append(out, e.Name())
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// assertRestorable checks that path holds a save this program can load,
|
||||
// which is what "the save was taken" has to mean: a file of the right
|
||||
// size proves nothing about a torn encode.
|
||||
func assertRestorable(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
|
||||
_, err := Restore(path, Params{Term: &testTerm{}})
|
||||
if err != nil {
|
||||
t.Errorf("the saved file does not restore: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// mkBlockedGame builds a game with a save file name and a terminal whose
|
||||
// reads block, for the tests that park the game goroutine.
|
||||
func mkBlockedGame(t *testing.T, term Terminal) *RogueGame {
|
||||
t.Helper()
|
||||
|
||||
g := New(Params{Seed: 4242, Term: term})
|
||||
g.NewLevel()
|
||||
g.FileName = filepath.Join(t.TempDir(), "rogue.save")
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// blockingTerm is a Terminal that genuinely blocks in ReadChar until a
|
||||
// key is pushed or Interrupt wakes it — which testTerm, whose reads never
|
||||
// block, cannot reproduce.
|
||||
type blockingTerm struct {
|
||||
keys chan byte
|
||||
wake chan struct{}
|
||||
}
|
||||
|
||||
func newBlockingTerm() *blockingTerm {
|
||||
return &blockingTerm{
|
||||
keys: make(chan byte),
|
||||
// Buffered by one and posted to without blocking, the same
|
||||
// contract term.Tcell.Interrupt has with tcell's event queue: an
|
||||
// interrupt that arrives before the read still wakes it.
|
||||
wake: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *blockingTerm) Render(*Window) {}
|
||||
|
||||
// Repaint has nothing to redraw: this terminal exists for its input
|
||||
// behaviour, and no autosave test types CTRL-R.
|
||||
func (t *blockingTerm) Repaint() {}
|
||||
|
||||
func (t *blockingTerm) Fini() {}
|
||||
|
||||
// Interrupt wakes a blocked ReadChar; called from the saving goroutine.
|
||||
func (t *blockingTerm) Interrupt() {
|
||||
select {
|
||||
case t.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// ReadChar blocks until a key arrives or Interrupt wakes it.
|
||||
func (t *blockingTerm) ReadChar() (byte, bool) {
|
||||
select {
|
||||
case ch := <-t.keys:
|
||||
return ch, true
|
||||
case <-t.wake:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// shellTerm is a blockingTerm that also offers a shell escape which stays
|
||||
// in the shell until the test lets it out.
|
||||
type shellTerm struct {
|
||||
*blockingTerm
|
||||
|
||||
entered chan struct{}
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
// ShellEscape parks the caller in the "shell" until released.
|
||||
func (t *shellTerm) ShellEscape() {
|
||||
close(t.entered)
|
||||
<-t.release
|
||||
}
|
||||
|
||||
// errShellResume is what panickingShellTerm panics with, standing in for
|
||||
// the value term.Tcell.ShellEscape raises when Screen.Resume fails.
|
||||
const errShellResume = "resume failed"
|
||||
|
||||
// panickingShellTerm is a blockingTerm whose shell escape panics on the
|
||||
// way out, the way term.Tcell.ShellEscape does when the screen cannot be
|
||||
// resumed. It records whether Fini ran, which is the thing that must
|
||||
// still happen.
|
||||
type panickingShellTerm struct {
|
||||
*blockingTerm
|
||||
|
||||
restored bool
|
||||
}
|
||||
|
||||
func (t *panickingShellTerm) Fini() { t.restored = true }
|
||||
|
||||
func (t *panickingShellTerm) ShellEscape() { panic(errShellResume) }
|
||||
@@ -1,497 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The bolt geometry of sticks.c fire_bolt, tested on the hand-carved
|
||||
// level built by mkCarvedGame in sticks_test.go.
|
||||
//
|
||||
// Most of these fire from a square that is not the hero's, which is what
|
||||
// chase.c does when a dragon breathes (fire_bolt(&th->t_pos, ...)). That
|
||||
// keeps the hero off the ray, so the run produces exactly one message
|
||||
// and the screen stays readable as a record of where the bolt went — see
|
||||
// litCells.
|
||||
|
||||
// The three names sticks.c fires a bolt under (do_zap's WS_ELECT,
|
||||
// WS_FIRE and WS_COLD arms); fire_bolt prints them and hangs them on the
|
||||
// FLAME weapon-table entry.
|
||||
const (
|
||||
boltName = "bolt"
|
||||
flameName = "flame"
|
||||
iceName = "ice"
|
||||
)
|
||||
|
||||
// litCells reports every non-blank cell of the map area of the screen.
|
||||
//
|
||||
// fire_bolt paints its trail with dirch and then erases it by writing
|
||||
// back chat() for each square it recorded, so on a screen nothing else
|
||||
// has drawn on, the squares left non-blank are exactly the ones the bolt
|
||||
// occupied. Squares it bounced off are absent by construction: C undoes
|
||||
// the record with c1-- and breaks before the mvaddch, so a wall is
|
||||
// neither painted nor erased.
|
||||
func litCells(g *RogueGame) []Coord {
|
||||
var out []Coord
|
||||
// Row 0 is the message line, not the map.
|
||||
for y := 1; y < NumLines; y++ {
|
||||
line := g.scr.Std.Line(y)
|
||||
for x := range len(line) {
|
||||
if line[x] != ' ' {
|
||||
out = append(out, Coord{X: x, Y: y})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// assertErased checks that every square the bolt flew over is showing
|
||||
// the map character underneath it again: fire_bolt's closing loop paints
|
||||
// chat() back over the whole trail, so a bolt leaves no '/' or '\'
|
||||
// behind.
|
||||
func assertErased(t *testing.T, g *RogueGame, cells []Coord) {
|
||||
t.Helper()
|
||||
|
||||
for _, c := range cells {
|
||||
got := g.scr.Std.Line(c.Y)[c.X]
|
||||
if want := g.Level.Char(c.Y, c.X); got != want {
|
||||
t.Errorf("square %v shows %q, want the map's %q: the trail "+
|
||||
"was not erased", c, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBoltDirChar covers the dirch switch for all eight directions. C
|
||||
// keys it on dir->y + dir->x: the two sums of zero are the '/' pair, the
|
||||
// two of magnitude two are the '\' pair, and the four axis directions
|
||||
// split on whether y is zero.
|
||||
func TestBoltDirChar(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dir Coord
|
||||
want byte
|
||||
}{
|
||||
{name: "north", dir: Coord{X: 0, Y: -1}, want: '|'},
|
||||
{name: "south", dir: Coord{X: 0, Y: 1}, want: '|'},
|
||||
{name: "east", dir: Coord{X: 1, Y: 0}, want: '-'},
|
||||
{name: "west", dir: Coord{X: -1, Y: 0}, want: '-'},
|
||||
{name: "north east", dir: Coord{X: 1, Y: -1}, want: '/'},
|
||||
{name: "south west", dir: Coord{X: -1, Y: 1}, want: '/'},
|
||||
{name: "north west", dir: Coord{X: -1, Y: -1}, want: '\\'},
|
||||
{name: "south east", dir: Coord{X: 1, Y: 1}, want: '\\'},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := boltDirChar(tt.dir); got != tt.want {
|
||||
t.Errorf("boltDirChar(%v) = %q, want %q", tt.dir, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBoltBounces covers the case labels a bolt reflects off, and the
|
||||
// door exception: C jumps to the default arm when the hero is standing
|
||||
// on the door, "otherwise it would loop infinitely".
|
||||
func TestBoltBounces(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const heroX, heroY = 5, 5
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ch byte
|
||||
pos Coord
|
||||
want bool
|
||||
}{
|
||||
{name: "vertical wall", ch: '|', pos: Coord{X: 6, Y: 5}, want: true},
|
||||
{name: "horizontal wall", ch: '-', pos: Coord{X: 6, Y: 5}, want: true},
|
||||
{name: "solid rock", ch: ' ', pos: Coord{X: 6, Y: 5}, want: true},
|
||||
{name: "door", ch: Door, pos: Coord{X: 6, Y: 5}, want: true},
|
||||
{
|
||||
name: "the door under the hero",
|
||||
ch: Door,
|
||||
pos: Coord{X: heroX, Y: heroY},
|
||||
want: false,
|
||||
},
|
||||
{name: "floor", ch: Floor, pos: Coord{X: 6, Y: 5}, want: false},
|
||||
{name: "passage", ch: Passage, pos: Coord{X: 6, Y: 5}, want: false},
|
||||
{name: "staircase", ch: Stairs, pos: Coord{X: 6, Y: 5}, want: false},
|
||||
{name: "a monster", ch: 'Z', pos: Coord{X: 6, Y: 5}, want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hero := Coord{X: heroX, Y: heroY}
|
||||
if got := boltBounces(tt.ch, hero, tt.pos); got != tt.want {
|
||||
t.Errorf("boltBounces(%q) = %v, want %v", tt.ch, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFireBoltFliesStraight is the end-to-end run with nothing in the
|
||||
// way: six squares, BOLT_LENGTH of them, and the last one is where the
|
||||
// bolt stops.
|
||||
func TestFireBoltFliesStraight(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 41)
|
||||
dir := Coord{X: 1, Y: 0}
|
||||
|
||||
g.fireBolt(Coord{X: 2, Y: 2}, &dir, flameName)
|
||||
|
||||
want := []Coord{
|
||||
{X: 3, Y: 2}, {X: 4, Y: 2}, {X: 5, Y: 2},
|
||||
{X: 6, Y: 2}, {X: 7, Y: 2}, {X: 8, Y: 2},
|
||||
}
|
||||
|
||||
got := litCells(g)
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("bolt path = %v, want %v", got, want)
|
||||
}
|
||||
|
||||
assertErased(t, g, got)
|
||||
|
||||
if g.Msgs.Huh != "" {
|
||||
t.Errorf("a bolt that hit nothing said %q", g.Msgs.Huh)
|
||||
}
|
||||
|
||||
if (dir != Coord{X: 1, Y: 0}) {
|
||||
t.Errorf("direction = %v, want it unchanged", dir)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFireBoltBounces covers the reflection rule in both wall
|
||||
// orientations, off a corner, and — the case that separates C's rule
|
||||
// from a plausible wrong one — diagonally off a vertical wall. C negates
|
||||
// *both* components, so a bolt that came in at 45 degrees goes back the
|
||||
// way it came instead of reflecting off the surface.
|
||||
// boltBounceCase is one wall-bounce run: where the bolt sets off, which
|
||||
// way it goes, the wall it must reflect off, and the squares it must end
|
||||
// up having occupied.
|
||||
type boltBounceCase struct {
|
||||
name string
|
||||
start Coord
|
||||
dir Coord
|
||||
wall Coord
|
||||
want []Coord
|
||||
}
|
||||
|
||||
// run fires the case's bolt and checks its whole flight.
|
||||
func (tt boltBounceCase) run(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
g := mkCarvedGame(t, 42)
|
||||
dir := tt.dir
|
||||
|
||||
g.fireBolt(tt.start, &dir, flameName)
|
||||
|
||||
got := litCells(g)
|
||||
if !slices.Equal(got, tt.want) {
|
||||
t.Errorf("bolt path = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
assertErased(t, g, got)
|
||||
|
||||
if slices.Contains(got, tt.wall) {
|
||||
t.Errorf("the wall at %v was drawn on; C drops the bounce "+
|
||||
"square from spotpos before the mvaddch", tt.wall)
|
||||
}
|
||||
|
||||
if want := (Coord{X: -tt.dir.X, Y: -tt.dir.Y}); dir != want {
|
||||
t.Errorf("direction = %v after one bounce, want %v", dir, want)
|
||||
}
|
||||
|
||||
if g.Msgs.Huh != "the flame bounces" {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, "the flame bounces")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFireBoltBounces(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []boltBounceCase{
|
||||
{
|
||||
name: "off a vertical wall",
|
||||
start: Coord{X: 3, Y: corridorY},
|
||||
dir: Coord{X: -1, Y: 0},
|
||||
wall: Coord{X: 1, Y: corridorY},
|
||||
// Five squares, not six: the square in front of the wall is
|
||||
// flown over twice, and C charges spotpos for both.
|
||||
want: []Coord{
|
||||
{X: 2, Y: 4}, {X: 3, Y: 4}, {X: 4, Y: 4},
|
||||
{X: 5, Y: 4}, {X: 6, Y: 4},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "off a horizontal wall",
|
||||
start: Coord{X: 5, Y: 2},
|
||||
dir: Coord{X: 0, Y: -1},
|
||||
wall: Coord{X: 5, Y: 1},
|
||||
want: []Coord{
|
||||
{X: 5, Y: 2}, {X: 5, Y: 3}, {X: 5, Y: 4},
|
||||
{X: 5, Y: 5}, {X: 5, Y: 6}, {X: 5, Y: 7},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "off a corner",
|
||||
start: Coord{X: 3, Y: 3},
|
||||
dir: Coord{X: -1, Y: -1},
|
||||
wall: Coord{X: 1, Y: 1},
|
||||
want: []Coord{
|
||||
{X: 2, Y: 2}, {X: 3, Y: 3}, {X: 4, Y: 4},
|
||||
{X: 5, Y: 5}, {X: 6, Y: 6},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "diagonally off a vertical wall",
|
||||
start: Coord{X: 3, Y: corridorY},
|
||||
dir: Coord{X: -1, Y: -1},
|
||||
wall: Coord{X: 1, Y: 2},
|
||||
want: []Coord{
|
||||
{X: 2, Y: 3}, {X: 3, Y: 4}, {X: 4, Y: 5},
|
||||
{X: 5, Y: 6}, {X: 6, Y: 7},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tt.run(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFireBoltReboundsIntoHero covers the hit_hero/changed pair: a bolt
|
||||
// the hero fires starts unable to hit him, and the first bounce flips
|
||||
// that, so a wall one square away throws his own bolt back at him.
|
||||
func TestFireBoltReboundsIntoHero(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lvl int
|
||||
wantMsg string
|
||||
wantHurt bool
|
||||
}{
|
||||
{
|
||||
name: "the hero saves",
|
||||
lvl: saveProofLvl,
|
||||
wantMsg: "the flame whizzes by you",
|
||||
wantHurt: false,
|
||||
},
|
||||
{
|
||||
name: "the hero is hit",
|
||||
lvl: 1,
|
||||
wantMsg: "you are hit by the flame",
|
||||
wantHurt: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 43)
|
||||
placeHero(g, Coord{X: roomAX + 1, Y: corridorY})
|
||||
fortify(g) // a bolt to the face must not exit the test binary
|
||||
g.Player.Stats.Lvl = tt.lvl
|
||||
|
||||
pinRng(t, g, d20, 1) // the lowest save throw there is
|
||||
|
||||
hp := g.Player.Stats.HP
|
||||
dir := Coord{X: -1, Y: 0}
|
||||
|
||||
g.fireBolt(g.Player.Pos, &dir, flameName)
|
||||
|
||||
if g.Msgs.Huh != tt.wantMsg {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, tt.wantMsg)
|
||||
}
|
||||
|
||||
lost := hp - g.Player.Stats.HP
|
||||
if hurt := lost > 0; hurt != tt.wantHurt {
|
||||
t.Errorf("hero lost %d hit points, want hurt = %v",
|
||||
lost, tt.wantHurt)
|
||||
}
|
||||
// roll(6, 6) is six to thirty-six.
|
||||
if tt.wantHurt && (lost < 6 || lost > 36) {
|
||||
t.Errorf("hero lost %d hit points, want 6..36", lost)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFireBoltFromDoorUnderHeroTerminates covers the guard C wrote the
|
||||
// door case for: the hero standing on a door and firing into the wall
|
||||
// that door sits in. Without the ce(hero, pos) exception the bolt
|
||||
// bounces on his own square forever, never recording a spot and never
|
||||
// filling spotpos, and fire_bolt does not return — this test hangs
|
||||
// rather than fails if the exception is lost.
|
||||
func TestFireBoltFromDoorUnderHeroTerminates(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 44)
|
||||
placeHero(g, Coord{X: doorAX, Y: corridorY})
|
||||
fortify(g)
|
||||
|
||||
pinRng(t, g, d20, 1) // no save: the strike ends the flight
|
||||
|
||||
hp := g.Player.Stats.HP
|
||||
dir := Coord{X: 0, Y: -1} // north, into the wall the door is in
|
||||
|
||||
g.fireBolt(g.Player.Pos, &dir, boltName)
|
||||
|
||||
if g.Msgs.Huh != "you are hit by the bolt" {
|
||||
t.Errorf("message = %q, want the hero to be hit", g.Msgs.Huh)
|
||||
}
|
||||
|
||||
if lost := hp - g.Player.Stats.HP; lost < 6 || lost > 36 {
|
||||
t.Errorf("hero lost %d hit points, want 6..36", lost)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFireBoltStrikesMonster covers the monster arm both ways, and the
|
||||
// dragon's immunity to flame that C spells out in the same breath.
|
||||
func TestFireBoltStrikesMonster(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
typ byte
|
||||
lvl int
|
||||
bolt string
|
||||
wantMsg string
|
||||
wantHurt bool
|
||||
}{
|
||||
{
|
||||
name: "it fails its save",
|
||||
typ: 'Z',
|
||||
lvl: 1,
|
||||
bolt: boltName,
|
||||
wantMsg: "the bolt hits the zombie",
|
||||
wantHurt: true,
|
||||
},
|
||||
{
|
||||
name: "it saves",
|
||||
typ: 'Z',
|
||||
lvl: saveProofLvl,
|
||||
bolt: boltName,
|
||||
wantMsg: "the bolt whizzes past the zombie",
|
||||
wantHurt: false,
|
||||
},
|
||||
{
|
||||
name: "a dragon shrugs off a flame",
|
||||
typ: 'D',
|
||||
lvl: 1,
|
||||
bolt: flameName,
|
||||
wantMsg: "the flame bounces off the dragon",
|
||||
wantHurt: false,
|
||||
},
|
||||
{
|
||||
name: "but not a lightning bolt",
|
||||
typ: 'D',
|
||||
lvl: 1,
|
||||
bolt: boltName,
|
||||
wantMsg: "the bolt hits the dragon",
|
||||
wantHurt: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 45)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
|
||||
tp := putMonster(g, tt.typ, Coord{X: 8, Y: corridorY})
|
||||
tp.Stats.Lvl = tt.lvl
|
||||
tp.Stats.HP = 500 // enough to survive 6x6 and stay assertable
|
||||
|
||||
pinRng(t, g, d20, 1) // the lowest save throw there is
|
||||
|
||||
dir := Coord{X: 1, Y: 0}
|
||||
g.fireBolt(g.Player.Pos, &dir, tt.bolt)
|
||||
|
||||
if g.Msgs.Huh != tt.wantMsg {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, tt.wantMsg)
|
||||
}
|
||||
|
||||
if hurt := tp.Stats.HP < 500; hurt != tt.wantHurt {
|
||||
t.Errorf("monster hit points = %d, want hurt = %v",
|
||||
tp.Stats.HP, tt.wantHurt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFireBoltMissedMonsterWakesUp covers the rest of the miss arm: a
|
||||
// bolt the hero fired sets the monster running (runto) before it says
|
||||
// what it whizzed past, and the bolt flies on for its full length.
|
||||
func TestFireBoltMissedMonsterWakesUp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 46)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
|
||||
tp := putMonster(g, 'Z', Coord{X: 8, Y: corridorY})
|
||||
tp.Stats.Lvl = saveProofLvl
|
||||
tp.Flags.Clear(Awake)
|
||||
|
||||
dir := Coord{X: 1, Y: 0}
|
||||
g.fireBolt(g.Player.Pos, &dir, boltName)
|
||||
|
||||
if !tp.On(Awake) {
|
||||
t.Error("the monster the bolt missed is still asleep")
|
||||
}
|
||||
|
||||
if tp.Dest != &g.Player.Pos {
|
||||
t.Error("the woken monster is not chasing the hero")
|
||||
}
|
||||
|
||||
if tp.OldCh != Floor {
|
||||
t.Errorf("under-character = %q, want %q: fire_bolt records chat() "+
|
||||
"before it resolves the save", tp.OldCh, Floor)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFireBoltMissSpeaksEvenForAnM pins the "ch != 'M' ||
|
||||
// tp->t_disguise == 'M'" guard on C's miss message, which reads as
|
||||
// though something looking like an 'M' can be missed silently. It
|
||||
// cannot: ch comes from winat, and winat *is* t_disguise whenever a
|
||||
// monster stands there (rogue.h 57), so ch == 'M' implies
|
||||
// t_disguise == 'M' and the condition is always true. The guard is
|
||||
// vestigial — 'M' was the mimic in earlier Rogues — and a port that
|
||||
// "tidied" it into a real silence would go quiet where C speaks.
|
||||
func TestFireBoltMissSpeaksEvenForAnM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 47)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
|
||||
tp := putMonster(g, 'M', Coord{X: 8, Y: corridorY})
|
||||
tp.Stats.Lvl = saveProofLvl
|
||||
tp.Flags.Clear(Awake)
|
||||
|
||||
dir := Coord{X: 1, Y: 0}
|
||||
g.fireBolt(g.Player.Pos, &dir, boltName)
|
||||
|
||||
const want = "the bolt whizzes past the medusa"
|
||||
if g.Msgs.Huh != want {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, want)
|
||||
}
|
||||
|
||||
if !tp.On(Awake) {
|
||||
t.Error("the missed medusa was not set running")
|
||||
}
|
||||
}
|
||||
522
game/chase.go
522
game/chase.go
@@ -1,522 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// chase.c — code for one creature to chase another.
|
||||
|
||||
// dragonShot: one chance in DRAGONSHOT that a dragon will flame.
|
||||
const dragonShot = 5
|
||||
|
||||
// runners makes all the running monsters move (chase.c runners).
|
||||
func (g *RogueGame) runners(int) {
|
||||
list := append([]*Monster(nil), g.Level.Monsters...)
|
||||
for _, tp := range list {
|
||||
g.runnerTurn(tp)
|
||||
}
|
||||
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
}
|
||||
}
|
||||
|
||||
// runnerTurn gives one monster its motion for the turn; flying monsters
|
||||
// far from the hero move twice (the loop body of chase.c runners).
|
||||
func (g *RogueGame) runnerTurn(tp *Monster) {
|
||||
if tp.On(Held) || !tp.On(Awake) {
|
||||
return
|
||||
}
|
||||
|
||||
origPos := tp.Pos
|
||||
|
||||
wastarget := tp.On(Targeted)
|
||||
if removed := g.moveMonster(tp); removed {
|
||||
return
|
||||
}
|
||||
|
||||
if tp.On(Flying) && distCp(g.Player.Pos, tp.Pos) >= 3 {
|
||||
if removed := g.moveMonster(tp); removed {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if wastarget && origPos != tp.Pos {
|
||||
tp.Flags.Clear(Targeted)
|
||||
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
|
||||
// moveMonster executes a single turn of running for a monster (chase.c
|
||||
// move_monst). The result reports that the monster died or left the
|
||||
// level (the C -1 return).
|
||||
func (g *RogueGame) moveMonster(tp *Monster) bool {
|
||||
if !tp.On(Slowed) || tp.Turn {
|
||||
if g.chaseStep(tp) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if tp.On(Hasted) {
|
||||
if g.chaseStep(tp) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
tp.Turn = !tp.Turn
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// relocate makes the monster's new location be the specified one, updating
|
||||
// all the relevant state (chase.c relocate).
|
||||
func (g *RogueGame) relocate(th *Monster, newLoc Coord) {
|
||||
if newLoc != th.Pos {
|
||||
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
|
||||
th.Room = g.roomIn(newLoc)
|
||||
g.setOldChar(th, newLoc)
|
||||
oroom := th.Room
|
||||
g.Level.SetMonsterAt(th.Pos.Y, th.Pos.X, nil)
|
||||
|
||||
if oroom != th.Room {
|
||||
th.Dest = g.findDest(th)
|
||||
}
|
||||
|
||||
th.Pos = newLoc
|
||||
g.Level.SetMonsterAt(newLoc.Y, newLoc.X, th)
|
||||
}
|
||||
|
||||
g.move(newLoc.Y, newLoc.X)
|
||||
|
||||
if g.seeMonst(th) {
|
||||
g.addch(th.Disguise)
|
||||
} else if g.Player.On(SenseMonsters) {
|
||||
g.standout()
|
||||
g.addch(th.Type)
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
// chaseStep makes one thing chase another (chase.c do_chase). The
|
||||
// result reports that the chaser died or left the level in the attempt
|
||||
// (the C -1 return).
|
||||
func (g *RogueGame) chaseStep(th *Monster) bool {
|
||||
stoprun := false // true means we are there
|
||||
|
||||
rer, ree, door := g.chaseRooms(th)
|
||||
|
||||
this, shot := g.chaseGoal(th, rer, ree, door, 32767)
|
||||
if shot {
|
||||
return false
|
||||
}
|
||||
// This now contains what we want to run to this time so we run to it.
|
||||
// If we hit it we either want to fight it or stop running
|
||||
if g.chase(th, this) {
|
||||
if th.Type == 'F' {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
switch this {
|
||||
case g.Player.Pos:
|
||||
return g.attack(th)
|
||||
case *th.Dest:
|
||||
g.chaseTakeObject(th)
|
||||
|
||||
stoprun = th.Type != 'F'
|
||||
}
|
||||
}
|
||||
|
||||
g.relocate(th, g.chRet)
|
||||
// And stop running if need be
|
||||
if stoprun && th.Pos == *th.Dest {
|
||||
th.Flags.Clear(Awake)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// chaseRooms finds the rooms of the chaser and its desire; doors do not
|
||||
// count as inside rooms here (the setup of chase.c do_chase).
|
||||
func (g *RogueGame) chaseRooms(th *Monster) (*Room, *Room, bool) {
|
||||
p := &g.Player
|
||||
|
||||
rer := th.Room // find room of chaser
|
||||
if th.On(Greedy) && rer.GoldVal == 0 {
|
||||
th.Dest = &p.Pos // if gold has been taken, run after hero
|
||||
}
|
||||
|
||||
var ree *Room // find room of chasee
|
||||
if th.Dest == &p.Pos {
|
||||
ree = p.Room
|
||||
} else {
|
||||
ree = g.roomIn(*th.Dest)
|
||||
}
|
||||
|
||||
return rer, ree, g.Level.Char(th.Pos.Y, th.Pos.X) == Door
|
||||
}
|
||||
|
||||
// chaseGoal picks the spot the chaser runs toward this turn: the
|
||||
// nearest exit toward its desire when it is in a different room, or the
|
||||
// desire itself. shot means a dragon breathed flame instead of moving
|
||||
// (the goal loop of chase.c do_chase).
|
||||
func (g *RogueGame) chaseGoal(
|
||||
th *Monster, rer, ree *Room, door bool, mindist int,
|
||||
) (Coord, bool) {
|
||||
var this Coord
|
||||
|
||||
for {
|
||||
// If the object of our desire is in a different room, and we are
|
||||
// not in a corridor, run to the door nearest to our goal.
|
||||
if rer == ree {
|
||||
this = *th.Dest
|
||||
|
||||
return this, g.dragonBreath(th)
|
||||
}
|
||||
|
||||
for i := range rer.Exits {
|
||||
curdist := distCp(*th.Dest, rer.Exits[i])
|
||||
if curdist < mindist {
|
||||
this = rer.Exits[i]
|
||||
mindist = curdist
|
||||
}
|
||||
}
|
||||
|
||||
if !door {
|
||||
return this, false
|
||||
}
|
||||
|
||||
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPassNum]
|
||||
door = false
|
||||
// the C goto over: redo with the passage as room
|
||||
}
|
||||
}
|
||||
|
||||
// dragonBreath checks whether a dragon shoots flame at the hero instead
|
||||
// of moving, and shoots it (the D block of chase.c do_chase).
|
||||
func (g *RogueGame) dragonBreath(th *Monster) bool {
|
||||
if th.Type != 'D' || !g.dragonShoots(th) {
|
||||
return false
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
|
||||
g.Delta.Y = sign(p.Pos.Y - th.Pos.Y)
|
||||
|
||||
g.Delta.X = sign(p.Pos.X - th.Pos.X)
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.fireBolt(th.Pos, &g.Delta, "flame")
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
|
||||
g.Quiet = 0
|
||||
if g.ToDeath && !th.On(Targeted) {
|
||||
g.ToDeath = false
|
||||
g.Kamikaze = false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// dragonShoots decides whether the dragon takes the shot: the hero is
|
||||
// on a straight line from it, within shooting distance but outside
|
||||
// striking range, it is not cancelled, and the shot roll comes up
|
||||
// (chase.c do_chase).
|
||||
func (g *RogueGame) dragonShoots(th *Monster) bool {
|
||||
p := &g.Player
|
||||
if th.Pos.Y != p.Pos.Y && th.Pos.X != p.Pos.X &&
|
||||
abs(th.Pos.Y-p.Pos.Y) != abs(th.Pos.X-p.Pos.X) {
|
||||
return false
|
||||
}
|
||||
|
||||
return distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
|
||||
!th.On(Cancelled) && g.rnd(dragonShot) == 0
|
||||
}
|
||||
|
||||
// chaseTakeObject has the monster pick up the object it was running to
|
||||
// (the dest arm of chase.c do_chase).
|
||||
func (g *RogueGame) chaseTakeObject(th *Monster) {
|
||||
for _, obj := range g.Level.Objects {
|
||||
if th.Dest == &obj.Pos {
|
||||
g.Level.RemoveObject(obj)
|
||||
attachObj(&th.Pack, obj)
|
||||
|
||||
if th.Room.Flags.Has(Gone) {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Floor)
|
||||
}
|
||||
|
||||
th.Dest = g.findDest(th)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// chase finds the spot for the chaser to move closer to the chasee
|
||||
// (chase.c chase). Returns true if we want to keep on chasing later, false
|
||||
// if we reach the goal. The chosen spot lands in g.chRet.
|
||||
func (g *RogueGame) chase(tp *Monster, ee Coord) bool {
|
||||
var curdist int
|
||||
|
||||
// If the thing is confused, let it move randomly. Invisible Stalkers
|
||||
// are slightly confused all of the time, and bats are quite confused
|
||||
// all the time
|
||||
if (tp.On(Confused) && g.rnd(5) != 0) || (tp.Type == 'P' && g.rnd(5) == 0) ||
|
||||
(tp.Type == 'B' && g.rnd(2) == 0) {
|
||||
// get a valid random move
|
||||
g.chRet = g.randomStep(&tp.Creature)
|
||||
curdist = distCp(g.chRet, ee)
|
||||
// Small chance that it will become un-confused
|
||||
if g.rnd(20) == 0 {
|
||||
tp.Flags.Clear(Confused)
|
||||
}
|
||||
} else {
|
||||
curdist = g.chaseBestSpot(tp, ee)
|
||||
}
|
||||
|
||||
return curdist != 0 && g.chRet != g.Player.Pos
|
||||
}
|
||||
|
||||
// chaseSearch is the scan state while chase looks for the step that
|
||||
// gets a monster closest to its chasee.
|
||||
type chaseSearch struct {
|
||||
er Coord // where the chaser is
|
||||
ee Coord // where it wants to go
|
||||
curdist int
|
||||
plcnt int
|
||||
}
|
||||
|
||||
// chaseBestSpot finds the empty spot next to the chaser that is closest
|
||||
// to the chasee, leaving it in g.chRet; if there is none, the chaser
|
||||
// stays where it is (the search half of chase.c chase).
|
||||
func (g *RogueGame) chaseBestSpot(tp *Monster, ee Coord) int {
|
||||
er := tp.Pos
|
||||
s := chaseSearch{er: er, ee: ee, curdist: distCp(er, ee), plcnt: 1}
|
||||
g.chRet = er
|
||||
|
||||
ey := er.Y + 1
|
||||
if ey >= NumLines-1 {
|
||||
ey = NumLines - 2
|
||||
}
|
||||
|
||||
ex := er.X + 1
|
||||
if ex >= NumCols {
|
||||
ex = NumCols - 1
|
||||
}
|
||||
|
||||
for x := er.X - 1; x <= ex; x++ {
|
||||
if x < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for y := er.Y - 1; y <= ey; y++ {
|
||||
g.chaseTry(&s, y, x)
|
||||
}
|
||||
}
|
||||
|
||||
return s.curdist
|
||||
}
|
||||
|
||||
// chaseTry scores one candidate square, reservoir-sampling among ties
|
||||
// (the scan body of chase.c chase).
|
||||
func (g *RogueGame) chaseTry(s *chaseSearch, y, x int) {
|
||||
tryp := Coord{X: x, Y: y}
|
||||
if !g.diagOk(s.er, tryp) {
|
||||
return
|
||||
}
|
||||
|
||||
ch := g.Level.VisibleChar(y, x)
|
||||
if !stepOk(ch) {
|
||||
return
|
||||
}
|
||||
// If it is a scroll, it might be a scare monster scroll so we need
|
||||
// to look it up to see what type it is.
|
||||
if ch == Scroll && g.scareScrollAt(y, x) {
|
||||
return
|
||||
}
|
||||
// It can also be a Xeroc, which we shouldn't step on
|
||||
if m := g.Level.MonsterAt(y, x); m != nil && m.Type == 'X' {
|
||||
return
|
||||
}
|
||||
// If we didn't find any scrolls at this place or it wasn't a scare
|
||||
// scroll, then this place counts
|
||||
thisdist := distance(y, x, s.ee.Y, s.ee.X)
|
||||
if thisdist < s.curdist {
|
||||
s.plcnt = 1
|
||||
g.chRet = tryp
|
||||
s.curdist = thisdist
|
||||
} else if thisdist == s.curdist {
|
||||
if s.plcnt++; g.rnd(s.plcnt) == 0 {
|
||||
g.chRet = tryp
|
||||
s.curdist = thisdist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scareScrollAt reports whether the object lying at (y, x) is a scare
|
||||
// monster scroll (chase.c chase).
|
||||
func (g *RogueGame) scareScrollAt(y, x int) bool {
|
||||
for _, obj := range g.Level.Objects {
|
||||
if y == obj.Pos.Y && x == obj.Pos.X {
|
||||
return obj.ScrollKind() == ScrollScareMonster
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// setOldChar sets the oldch character for the monster (chase.c set_oldch).
|
||||
func (g *RogueGame) setOldChar(tp *Monster, cp Coord) {
|
||||
if tp.Pos == cp {
|
||||
return
|
||||
}
|
||||
|
||||
sch := tp.OldCh
|
||||
|
||||
tp.OldCh = g.mvinch(cp.Y, cp.X)
|
||||
if !g.Player.On(Blind) {
|
||||
if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(Dark) {
|
||||
tp.OldCh = ' '
|
||||
} else if distCp(cp, g.Player.Pos) <= LampDist && g.Options.SeeFloor {
|
||||
tp.OldCh = g.Level.Char(cp.Y, cp.X)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// seeMonst returns true if the hero can see the monster (chase.c
|
||||
// see_monst).
|
||||
func (g *RogueGame) seeMonst(mp *Monster) bool {
|
||||
p := &g.Player
|
||||
if p.On(Blind) {
|
||||
return false
|
||||
}
|
||||
|
||||
if mp.On(Invisible) && !p.On(CanSeeInvisible) {
|
||||
return false
|
||||
}
|
||||
|
||||
y, x := mp.Pos.Y, mp.Pos.X
|
||||
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
if y != p.Pos.Y && x != p.Pos.X &&
|
||||
!stepOk(g.Level.Char(y, p.Pos.X)) && !stepOk(g.Level.Char(p.Pos.Y, x)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if mp.Room != p.Room {
|
||||
return false
|
||||
}
|
||||
|
||||
return !mp.Room.Flags.Has(Dark)
|
||||
}
|
||||
|
||||
// runTo sets a monster running after the hero (chase.c runto).
|
||||
func (g *RogueGame) runTo(runner Coord) {
|
||||
tp := g.Level.MonsterAt(runner.Y, runner.X)
|
||||
if tp == nil {
|
||||
return
|
||||
}
|
||||
// Start the beastie running
|
||||
tp.Flags.Set(Awake)
|
||||
tp.Flags.Clear(Held)
|
||||
tp.Dest = g.findDest(tp)
|
||||
}
|
||||
|
||||
// roomIn finds what room some coordinates are in; nil means they aren't in
|
||||
// any room (chase.c roomin).
|
||||
func (g *RogueGame) roomIn(cp Coord) *Room {
|
||||
fp := *g.Level.FlagsAt(cp.Y, cp.X)
|
||||
if fp.Has(FPassage) {
|
||||
return &g.Level.Passages[fp&FPassNum]
|
||||
}
|
||||
|
||||
for i := range g.Level.Rooms {
|
||||
rp := &g.Level.Rooms[i]
|
||||
if cp.X <= rp.Pos.X+rp.Max.X && rp.Pos.X <= cp.X &&
|
||||
cp.Y <= rp.Pos.Y+rp.Max.Y && rp.Pos.Y <= cp.Y {
|
||||
return rp
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("in some bizarre place (%d, %d)", cp.Y, cp.X)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// diagOk checks to see if a diagonal move is legal (chase.c diag_ok).
|
||||
func (g *RogueGame) diagOk(sp, ep Coord) bool {
|
||||
if ep.X < 0 || ep.X >= NumCols || ep.Y <= 0 || ep.Y >= NumLines-1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if ep.X == sp.X || ep.Y == sp.Y {
|
||||
return true
|
||||
}
|
||||
|
||||
return stepOk(g.Level.Char(ep.Y, sp.X)) && stepOk(g.Level.Char(sp.Y, ep.X))
|
||||
}
|
||||
|
||||
// canSee returns true if the hero can see a certain coordinate (chase.c
|
||||
// cansee).
|
||||
func (g *RogueGame) canSee(y, x int) bool {
|
||||
p := &g.Player
|
||||
if p.On(Blind) {
|
||||
return false
|
||||
}
|
||||
|
||||
if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist {
|
||||
if g.Level.FlagsAt(y, x).Has(FPassage) {
|
||||
if y != p.Pos.Y && x != p.Pos.X &&
|
||||
!stepOk(g.Level.Char(y, p.Pos.X)) &&
|
||||
!stepOk(g.Level.Char(p.Pos.Y, x)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
// We can only see if the hero is in the same room as the coordinate
|
||||
// and the room is lit, or if it is close.
|
||||
rer := g.roomIn(Coord{X: x, Y: y})
|
||||
|
||||
return rer == p.Room && !rer.Flags.Has(Dark)
|
||||
}
|
||||
|
||||
// findDest finds the proper destination for the monster (chase.c
|
||||
// find_dest).
|
||||
func (g *RogueGame) findDest(tp *Monster) *Coord {
|
||||
prob := g.Monsters[tp.Type-'A'].Carry
|
||||
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
|
||||
return &g.Player.Pos
|
||||
}
|
||||
|
||||
for _, obj := range g.Level.Objects {
|
||||
if obj.Kind == KindScroll && obj.ScrollKind() == ScrollScareMonster {
|
||||
continue
|
||||
}
|
||||
|
||||
if g.roomIn(obj.Pos) == tp.Room && g.rnd(100) < prob &&
|
||||
!g.objectClaimed(obj) {
|
||||
return &obj.Pos
|
||||
}
|
||||
}
|
||||
|
||||
return &g.Player.Pos
|
||||
}
|
||||
|
||||
// objectClaimed reports whether some monster already runs toward this
|
||||
// object (chase.c find_dest).
|
||||
func (g *RogueGame) objectClaimed(obj *Object) bool {
|
||||
for _, other := range g.Level.Monsters {
|
||||
if other.Dest == &obj.Pos {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
992
game/command.go
992
game/command.go
@@ -1,992 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
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
|
||||
|
||||
// Between turns is the one point in the loop where the game state is
|
||||
// whole, so it is where a signal-triggered autosave is answered when
|
||||
// the game goroutine is busy rather than waiting for a key (issue
|
||||
// #24). The other service points are readchar (io.c) and
|
||||
// runShellEscape, covering the two ways this goroutine can be parked.
|
||||
g.serviceAutoSaveRequest()
|
||||
|
||||
ntimes := 1 // number of player moves
|
||||
if p.On(Hasted) {
|
||||
ntimes++
|
||||
}
|
||||
// Let the daemons start up
|
||||
g.DoDaemons(Before)
|
||||
g.DoFuses(Before)
|
||||
|
||||
for ; ntimes > 0; ntimes-- {
|
||||
g.playTurn()
|
||||
|
||||
if !g.After {
|
||||
ntimes++
|
||||
}
|
||||
}
|
||||
|
||||
g.DoDaemons(After)
|
||||
g.DoFuses(After)
|
||||
|
||||
g.ringTurnEffects(Left)
|
||||
g.ringTurnEffects(Right)
|
||||
}
|
||||
|
||||
// ringTurnEffects fires the per-turn ring powers on one hand: searching
|
||||
// searches, teleportation teleports on 1 turn in 50 (the tail of
|
||||
// command.c command).
|
||||
func (g *RogueGame) ringTurnEffects(hand int) {
|
||||
p := &g.Player
|
||||
if p.IsRing(hand, RingSearching) {
|
||||
g.search()
|
||||
} else if p.IsRing(hand, RingTeleportation) && g.rnd(50) == 0 {
|
||||
g.teleport()
|
||||
}
|
||||
}
|
||||
|
||||
// playTurn runs one full player turn: upkeep, one command, and pickup
|
||||
// (the body of the command.c command loop).
|
||||
func (g *RogueGame) playTurn() {
|
||||
p := &g.Player
|
||||
|
||||
g.turnUpkeep()
|
||||
|
||||
var ch byte
|
||||
|
||||
if g.NoCommand == 0 {
|
||||
ch = g.readCommand()
|
||||
} else {
|
||||
ch = '.'
|
||||
}
|
||||
|
||||
if g.NoCommand != 0 {
|
||||
if g.NoCommand--; g.NoCommand == 0 {
|
||||
p.Flags.Set(Awake)
|
||||
g.msg("you can move again")
|
||||
}
|
||||
} else {
|
||||
g.executeCommand(ch)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// turnUpkeep redraws and resets per-turn state before a command is read
|
||||
// (the top of the command.c command loop).
|
||||
func (g *RogueGame) turnUpkeep() {
|
||||
p := &g.Player
|
||||
|
||||
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(Slowed | Greedy | Invisible | Regenerates | Targeted) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// readCommand picks the next command character: run/count repeats or
|
||||
// fresh input (the read block of command.c command).
|
||||
func (g *RogueGame) readCommand() byte {
|
||||
switch {
|
||||
case g.Running || g.ToDeath:
|
||||
return g.RunCh
|
||||
case g.Count != 0:
|
||||
return g.countCh
|
||||
default:
|
||||
ch := g.readchar()
|
||||
|
||||
g.MoveOn = false
|
||||
if g.Msgs.Mpos != 0 { // erase message if its there
|
||||
g.msg("")
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
}
|
||||
|
||||
// executeCommand runs one read command with the repeat-count prefix and
|
||||
// the repeat-history bookkeeping (the execute block of command.c
|
||||
// command).
|
||||
func (g *RogueGame) executeCommand(ch byte) {
|
||||
// check for prefixes
|
||||
g.newCount = false
|
||||
ch = g.countPrefix(ch)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// countPrefix consumes a numeric repeat-count prefix and returns the
|
||||
// command character that follows it (command.c).
|
||||
func (g *RogueGame) countPrefix(ch byte) byte {
|
||||
if !isDigit(ch) {
|
||||
return ch
|
||||
}
|
||||
|
||||
g.Count = 0
|
||||
|
||||
g.newCount = true
|
||||
for isDigit(ch) {
|
||||
g.Count = min(g.Count*10+int(ch-'0'), 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
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// dispatch runs one command character, re-running dispatchKey when it
|
||||
// asks (the C `goto over` re-dispatch in command.c).
|
||||
func (g *RogueGame) dispatch(ch byte) {
|
||||
for {
|
||||
newCh, again := g.dispatchKey(ch)
|
||||
if !again {
|
||||
return
|
||||
}
|
||||
|
||||
ch = newCh
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchKey runs one command character (the big switch of command.c).
|
||||
// A true second result asks dispatch to run once more with the returned
|
||||
// key.
|
||||
func (g *RogueGame) dispatchKey(ch byte) (byte, bool) {
|
||||
if h, ok := g.data.commandHandlers[ch]; ok {
|
||||
h(g)
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'),
|
||||
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'):
|
||||
return g.runCommand(ch)
|
||||
case 'F', 'f':
|
||||
return g.fightCommand(ch)
|
||||
case 'a':
|
||||
return g.repeatCommand()
|
||||
case 'm':
|
||||
return g.moveOnCommand()
|
||||
default:
|
||||
g.After = false
|
||||
if g.Wizard {
|
||||
g.wizardCommand(ch)
|
||||
} else {
|
||||
g.illcom(ch)
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// runCommand handles the ctrl-direction run-until-adjacent prefix; the
|
||||
// returned key re-dispatches as the matching run command (command.c).
|
||||
func (g *RogueGame) runCommand(ch byte) (byte, bool) {
|
||||
if !g.Player.On(Blind) {
|
||||
g.DoorStop = true
|
||||
g.Firstmove = true
|
||||
}
|
||||
|
||||
if g.Count != 0 && !g.newCount {
|
||||
ch = g.direction
|
||||
} else {
|
||||
ch += 'A' - CTRL('A')
|
||||
g.direction = ch
|
||||
}
|
||||
|
||||
return ch, true // the C goto over: re-dispatch as the run command
|
||||
}
|
||||
|
||||
// repeatCommand handles 'a': replay the last command by re-dispatching
|
||||
// it (command.c).
|
||||
func (g *RogueGame) repeatCommand() (byte, bool) {
|
||||
if g.LastComm == 0 {
|
||||
g.msg("you haven't typed a command yet")
|
||||
g.After = false
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
g.Again = true
|
||||
|
||||
return g.LastComm, true // the C goto over: replay the last command
|
||||
}
|
||||
|
||||
// moveOnCommand handles 'm': move without picking up; the direction key
|
||||
// re-dispatches (command.c).
|
||||
func (g *RogueGame) moveOnCommand() (byte, bool) {
|
||||
g.MoveOn = true
|
||||
if !g.promptDirection() {
|
||||
g.After = false
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
g.countCh = g.DirCh
|
||||
|
||||
return g.DirCh, true // the C goto over: move onto it without pickup
|
||||
}
|
||||
|
||||
// pickupCommand handles ',': pick up whatever is here (command.c).
|
||||
func (g *RogueGame) pickupCommand() {
|
||||
p := &g.Player
|
||||
|
||||
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.Kind.Glyph())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("there is ")
|
||||
}
|
||||
|
||||
g.addmsgf("nothing here")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" to pick up")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
// fightCommand handles the f/F fight prefix; a true again asks dispatch
|
||||
// to rerun with the direction key (command.c).
|
||||
func (g *RogueGame) fightCommand(ch byte) (byte, bool) {
|
||||
p := &g.Player
|
||||
if ch == 'F' {
|
||||
g.Kamikaze = true
|
||||
}
|
||||
|
||||
if !g.promptDirection() {
|
||||
g.After = false
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
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(SenseMonsters)) {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("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(Targeted)
|
||||
|
||||
g.RunCh = g.DirCh
|
||||
|
||||
return g.DirCh, true // the C goto over: fight by running at it
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// identifyTrapCommand handles '^': name the trap in a direction
|
||||
// (command.c).
|
||||
func (g *RogueGame) identifyTrapCommand() {
|
||||
p := &g.Player
|
||||
|
||||
g.After = false
|
||||
if !g.promptDirection() {
|
||||
return
|
||||
}
|
||||
|
||||
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.addmsgf("You have found ")
|
||||
}
|
||||
|
||||
switch {
|
||||
case g.Level.Char(g.Delta.Y, g.Delta.X) != Trap:
|
||||
g.msg("no trap there")
|
||||
case p.On(Hallucinating):
|
||||
g.msg("%s", g.data.trName[g.rnd(NumTrapTypes)])
|
||||
default:
|
||||
g.msg("%s", g.data.trName[*fp&FTrapMask])
|
||||
fp.Set(FSeen)
|
||||
}
|
||||
}
|
||||
|
||||
// wizardToggleCommand handles '+': leave wizard mode (command.c
|
||||
// command). C's arm lives in the main command switch under
|
||||
// #ifdef MASTER, not in the wizard sub-switch, so it is reachable
|
||||
// whether or not wizard is set.
|
||||
//
|
||||
// The entry half is deliberately not ported. C ran wizard = passwd(),
|
||||
// which compared a DES-crypted answer against a compiled-in password;
|
||||
// this port drops that machinery and makes wizard mode configuration
|
||||
// instead (ROGUE_WIZARD, ARCHITECTURE.md §9). A password check that is
|
||||
// gone is a password check that can never succeed, so the else arm
|
||||
// reduces to exactly what C did when the answer was wrong: wizard stays
|
||||
// off and the game says "sorry". No prompt is shown, since nothing typed
|
||||
// into it could change the outcome, and the noscore/turn_see(FALSE)
|
||||
// bookkeeping of C's success branch is unreachable and so is absent.
|
||||
func (g *RogueGame) wizardToggleCommand() {
|
||||
g.After = false
|
||||
|
||||
if !g.Wizard {
|
||||
g.msg("sorry")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.Wizard = false
|
||||
// Re-hide the monsters wizard sight was showing: without this there
|
||||
// is no way back to normal visibility.
|
||||
g.turnSee(true)
|
||||
g.msg("not wizard any more")
|
||||
}
|
||||
|
||||
// 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()
|
||||
default:
|
||||
g.wizardDebugCommand(ch)
|
||||
}
|
||||
}
|
||||
|
||||
// wizardDebugCommand handles the rest of the MASTER debug commands
|
||||
// (command.c).
|
||||
func (g *RogueGame) wizardDebugCommand(ch byte) {
|
||||
p := &g.Player
|
||||
|
||||
switch ch {
|
||||
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(SenseMonsters))
|
||||
case CTRL('~'):
|
||||
if item, ok := g.promptPackItem("charge", KindWand); ok {
|
||||
item.Charges = 10000
|
||||
}
|
||||
case CTRL('I'):
|
||||
g.wizardKit()
|
||||
case '*':
|
||||
g.prList()
|
||||
default:
|
||||
g.illcom(ch)
|
||||
}
|
||||
}
|
||||
|
||||
// wizardKit levels the wizard up and equips him (the ^I case of the
|
||||
// command.c wizard switch).
|
||||
func (g *RogueGame) wizardKit() {
|
||||
p := &g.Player
|
||||
|
||||
for range 9 {
|
||||
g.raiseLevel()
|
||||
}
|
||||
// Give him a sword (+1,+1)
|
||||
obj := newObject()
|
||||
g.initWeapon(obj, WeaponTwoHandedSword)
|
||||
obj.HPlus = 1
|
||||
obj.DPlus = 1
|
||||
g.addPack(obj, true)
|
||||
p.CurWeapon = obj
|
||||
// And his suit of armor
|
||||
obj = newObject()
|
||||
obj.Kind = KindArmor
|
||||
obj.Which = int(ArmorPlateMail)
|
||||
obj.ArmorClass = -5
|
||||
obj.Flags.Set(Known)
|
||||
obj.Count = 1
|
||||
p.CurArmor = obj
|
||||
g.addPack(obj, true)
|
||||
}
|
||||
|
||||
// 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(Hallucinating) {
|
||||
probinc = 3
|
||||
}
|
||||
|
||||
if p.On(Blind) {
|
||||
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
|
||||
}
|
||||
|
||||
if g.searchSpot(y, x, probinc) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
g.look(false)
|
||||
}
|
||||
}
|
||||
|
||||
// searchSpot tries to reveal one hidden thing next to the hero: a
|
||||
// secret door, a hidden trap, or a secret passage (the loop body of
|
||||
// command.c search). It reports whether something was found.
|
||||
func (g *RogueGame) searchSpot(y, x, probinc int) bool {
|
||||
fp := g.Level.FlagsAt(y, x)
|
||||
if fp.Has(FReal) {
|
||||
return false
|
||||
}
|
||||
|
||||
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:
|
||||
foundone = g.searchFloor(fp, y, x, probinc)
|
||||
case ' ':
|
||||
if g.rnd(3+probinc) != 0 {
|
||||
break
|
||||
}
|
||||
|
||||
g.Level.SetChar(y, x, Passage)
|
||||
|
||||
foundone = true
|
||||
}
|
||||
|
||||
if foundone {
|
||||
fp.Set(FReal)
|
||||
|
||||
g.Count = 0
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
return foundone
|
||||
}
|
||||
|
||||
// searchFloor reveals a hidden trap underfoot and names it (the FLOOR
|
||||
// arm of command.c search).
|
||||
func (g *RogueGame) searchFloor(fp *PlaceFlags, y, x, probinc int) bool {
|
||||
if g.rnd(2+probinc) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
g.Level.SetChar(y, x, Trap)
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you found ")
|
||||
}
|
||||
|
||||
if g.Player.On(Hallucinating) {
|
||||
g.msg("%s", g.data.trName[g.rnd(NumTrapTypes)])
|
||||
} else {
|
||||
g.msg("%s", g.data.trName[*fp&FTrapMask])
|
||||
fp.Set(FSeen)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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.helpOne(helpch)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.helpAll()
|
||||
}
|
||||
|
||||
// helpOne prints the help line for a single key, or an error for a
|
||||
// funny character (command.c help).
|
||||
func (g *RogueGame) helpOne(helpch byte) {
|
||||
g.move(0, 0)
|
||||
|
||||
for _, strp := range g.data.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))
|
||||
}
|
||||
|
||||
// helpAll prints help for everything in two columns, then waits before
|
||||
// returning to command mode (command.c help).
|
||||
func (g *RogueGame) helpAll() {
|
||||
numprint := g.helpLines()
|
||||
|
||||
hw := g.scr.Hw
|
||||
hw.Clear()
|
||||
|
||||
cnt := 0
|
||||
|
||||
for _, strp := range g.data.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()
|
||||
}
|
||||
|
||||
// helpLines counts how many rows the two-column help listing needs
|
||||
// (command.c help).
|
||||
func (g *RogueGame) helpLines() int {
|
||||
numprint := 0
|
||||
|
||||
for _, strp := range g.data.helpStr {
|
||||
if strp.Print {
|
||||
numprint++
|
||||
}
|
||||
}
|
||||
|
||||
if numprint&1 == 1 { // round odd numbers up
|
||||
numprint++
|
||||
}
|
||||
|
||||
numprint /= 2
|
||||
if numprint > NumLines-1 {
|
||||
numprint = NumLines - 1
|
||||
}
|
||||
|
||||
return numprint
|
||||
}
|
||||
|
||||
// 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 g.data.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 {
|
||||
g.msg("I see no way up")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !g.HasAmulet {
|
||||
g.msg("your way is magically blocked")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.Depth--
|
||||
if g.Depth == 0 {
|
||||
g.totalWinner()
|
||||
}
|
||||
|
||||
g.NewLevel()
|
||||
g.msg("you feel a wrenching sensation in your gut")
|
||||
}
|
||||
|
||||
// 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(Levitating) {
|
||||
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, ok := g.promptPackItem("call", KindCallable)
|
||||
// Make certain that it is something that we want to name
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
op, elsewise, guess, ok := g.callTarget(obj)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
elsewise, guess, ok = g.callPrelude(op, elsewise, guess)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// callTarget picks what a call names: the lore record for identifiable
|
||||
// kinds, the object's own label otherwise; ok is false for food (the
|
||||
// switch of command.c call).
|
||||
func (g *RogueGame) callTarget(obj *Object) (*ObjInfo, string, *string, bool) {
|
||||
it := &g.Items
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.Kind {
|
||||
case KindRing:
|
||||
return &it.Rings[obj.Which], it.RingStones[obj.Which], nil, true
|
||||
case KindPotion:
|
||||
return &it.Potions[obj.Which], it.PotColors[obj.Which], nil, true
|
||||
case KindScroll:
|
||||
return &it.Scrolls[obj.Which], it.ScrNames[obj.Which], nil, true
|
||||
case KindWand:
|
||||
return &it.Sticks[obj.Which], it.WandMade[obj.Which], nil, true
|
||||
case KindFood:
|
||||
g.msg("you can't call that anything")
|
||||
|
||||
return nil, "", nil, false
|
||||
default:
|
||||
return nil, obj.Label, &obj.Label, true
|
||||
}
|
||||
}
|
||||
|
||||
// callPrelude resolves the item's current name, reporting a previous
|
||||
// guess; ok is false when the item is already identified (command.c
|
||||
// call).
|
||||
func (g *RogueGame) callPrelude(
|
||||
op *ObjInfo, elsewise string, guess *string,
|
||||
) (string, *string, bool) {
|
||||
fromGuess := false
|
||||
|
||||
if op != nil {
|
||||
if op.Know {
|
||||
g.msg("that has already been identified")
|
||||
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
guess = &op.Guess
|
||||
if *guess != "" {
|
||||
elsewise = *guess
|
||||
fromGuess = true
|
||||
}
|
||||
} else {
|
||||
fromGuess = elsewise != "" && elsewise == *guess
|
||||
}
|
||||
|
||||
if fromGuess {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("Was ")
|
||||
}
|
||||
|
||||
g.msg("called \"%s\"", elsewise)
|
||||
}
|
||||
|
||||
return elsewise, guess, true
|
||||
}
|
||||
|
||||
// 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.addmsgf("you are ")
|
||||
}
|
||||
|
||||
g.addmsgf("%s nothing", how)
|
||||
|
||||
if where != "" {
|
||||
g.addmsgf(" %s", where)
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you are %s (", how)
|
||||
}
|
||||
|
||||
g.InvDescribe = false
|
||||
g.addmsgf("%c) %s", cur.PackCh, g.inventoryName(cur, true))
|
||||
|
||||
g.InvDescribe = true
|
||||
if where != "" {
|
||||
g.addmsgf(" %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
|
||||
|
||||
g.runShellEscape(se)
|
||||
|
||||
g.InShell = false
|
||||
g.refresh()
|
||||
} else {
|
||||
g.msg("shell escape is not available")
|
||||
}
|
||||
}
|
||||
|
||||
// runShellEscape runs the shell and returns when it exits, answering
|
||||
// signal-triggered autosave requests in the meantime (issue #24).
|
||||
//
|
||||
// The shell blocks for as long as the player is away — minutes, or until
|
||||
// they forget — and a line dropping while they are in it is exactly the
|
||||
// case SIGHUP autosave exists for, so this goroutine cannot simply sit
|
||||
// inside the call. The shell runs on a helper goroutine and the game
|
||||
// goroutine waits here, still the only one that ever encodes game state.
|
||||
// It draws nothing while it waits, so the suspend/resume dance is as
|
||||
// undisturbed as it was when it ran inline (ARCHITECTURE.md section 9).
|
||||
//
|
||||
// A panic out of ShellEscape must not be allowed to unwind on the helper
|
||||
// goroutine. term.Tcell.ShellEscape panics when Screen.Resume fails, and
|
||||
// a panic reaching the top of any goroutine kills the process without
|
||||
// running any *other* goroutine's deferred calls — which is where
|
||||
// cmd/rogue/main.go's `defer t.Fini()` lives. Running the shell off the
|
||||
// game goroutine would therefore have left the tty raw on exactly the
|
||||
// path where the terminal is already broken, reintroducing issue #12 on a
|
||||
// path this change created. So the helper recovers, and the value is
|
||||
// re-raised below on the game goroutine, whose stack does have Fini in
|
||||
// it. The recover deferral is registered after `defer close(done)` and so
|
||||
// runs before it, which is what publishes panicVal to the reader.
|
||||
func (g *RogueGame) runShellEscape(se interface{ ShellEscape() }) {
|
||||
done := make(chan struct{})
|
||||
|
||||
var panicVal any
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
|
||||
defer func() {
|
||||
panicVal = recover()
|
||||
}()
|
||||
|
||||
se.ShellEscape()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
if panicVal != nil {
|
||||
// Re-raised here so the unwind passes through the game
|
||||
// goroutine's deferred Fini. shell()'s InShell reset and
|
||||
// refresh are skipped deliberately: there is no screen
|
||||
// left to draw into.
|
||||
panic(panicVal)
|
||||
}
|
||||
|
||||
return
|
||||
case req := <-g.sigSave:
|
||||
g.runAutoSaveRequest(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestRedrawCommandForcesFullRepaint pins CTRL-R to a forced repaint
|
||||
// rather than an ordinary refresh. C's arm is "after = FALSE;
|
||||
// clearok(curscr, TRUE); wrefresh(curscr);" (command.c), and the
|
||||
// clearok is the command: a diffing refresh compares the new frame
|
||||
// against the device's record of the old one and sends nothing when they
|
||||
// agree, which is exactly the situation after some other program has
|
||||
// scribbled on the terminal. Only the terminal can tell the difference,
|
||||
// so the test watches the terminal rather than the window contents.
|
||||
func TestRedrawCommandForcesFullRepaint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
|
||||
term, ok := g.scr.term.(*testTerm)
|
||||
if !ok {
|
||||
t.Fatal("game terminal is not a testTerm")
|
||||
}
|
||||
|
||||
before := term.repaints
|
||||
g.After = true
|
||||
|
||||
g.dispatch(CTRL('R'))
|
||||
|
||||
if term.repaints != before+1 {
|
||||
t.Errorf("terminal repainted %d times, want %d: CTRL-R did not force "+
|
||||
"a full redraw", term.repaints-before, 1)
|
||||
}
|
||||
|
||||
if g.After {
|
||||
t.Error("CTRL-R consumed a turn; C sets after = FALSE")
|
||||
}
|
||||
}
|
||||
110
game/creature.go
110
game/creature.go
@@ -1,110 +0,0 @@
|
||||
package game
|
||||
|
||||
// Creature is the _t arm of the C THING union: the player or a monster.
|
||||
type Creature struct {
|
||||
Pos Coord // position
|
||||
Turn bool // if slowed, is it a turn to move
|
||||
Type byte // what it is: 'A'..'Z' for monsters, '@' for the player
|
||||
Disguise byte // what mimic looks like
|
||||
OldCh byte // character that was where it was
|
||||
// Dest is where it is running to — aliases live coords (hero pos,
|
||||
// room gold, another monster's pos).
|
||||
Dest *Coord
|
||||
Flags CreatureFlags
|
||||
Stats Stats
|
||||
Room *Room // current room for thing
|
||||
Pack []*Object // what the thing is carrying
|
||||
}
|
||||
|
||||
// Monster is a hostile creature on the level.
|
||||
type Monster struct {
|
||||
Creature
|
||||
}
|
||||
|
||||
// Player embeds Creature and owns the player-only state that was global
|
||||
// in the C sources (cur_armor, purse, food_left, ...).
|
||||
type Player struct {
|
||||
Creature
|
||||
|
||||
CurArmor *Object // what he is wearing
|
||||
CurWeapon *Object // which weapon he is wielding
|
||||
CurRing [2]*Object // which rings are being worn (Left/Right)
|
||||
PackUsed [26]bool // is the character used in the pack?
|
||||
Inpack int // number of things in pack
|
||||
Purse int // how much gold he has
|
||||
FoodLeft int // amount of food in hero's stomach
|
||||
HungryState int // how hungry is he (0..3)
|
||||
NoFood int // number of levels without food
|
||||
MaxStats Stats // the maximum for the player
|
||||
VfHit int // number of times the flytrap has hit
|
||||
}
|
||||
|
||||
// On is the C on(thing, flag) macro.
|
||||
func (c *Creature) On(f CreatureFlags) bool { return c.Flags.Has(f) }
|
||||
|
||||
// IsRing is the C ISRING(hand, ring) macro.
|
||||
func (p *Player) IsRing(hand int, ring RingKind) bool {
|
||||
return p.CurRing[hand] != nil && p.CurRing[hand].RingKind() == ring
|
||||
}
|
||||
|
||||
// IsWearing is the C ISWEARING(ring) macro: is the ring on either hand.
|
||||
func (p *Player) IsWearing(ring RingKind) bool {
|
||||
return p.IsRing(Left, ring) || p.IsRing(Right, ring)
|
||||
}
|
||||
|
||||
// nextPackChar claims and returns the next unused pack character (pack.c
|
||||
// pack_char).
|
||||
func (p *Player) nextPackChar() byte {
|
||||
for i := range p.PackUsed {
|
||||
if !p.PackUsed[i] {
|
||||
p.PackUsed[i] = true
|
||||
|
||||
return byte(i) + 'a'
|
||||
}
|
||||
}
|
||||
|
||||
return byte(len(p.PackUsed)) + 'a' // C would walk off the array here
|
||||
}
|
||||
|
||||
// removeFromPack takes an item out of the pack: the whole entry, or one
|
||||
// of a stack when all is false (the bookkeeping half of pack.c
|
||||
// leave_pack). It returns the object that left the pack — a copy when
|
||||
// newobj asks for a split.
|
||||
func (p *Player) removeFromPack(obj *Object, newobj, all bool) *Object {
|
||||
p.Inpack--
|
||||
|
||||
nobj := obj
|
||||
if obj.Count > 1 && !all {
|
||||
obj.Count--
|
||||
if obj.Group != 0 {
|
||||
p.Inpack++
|
||||
}
|
||||
|
||||
if newobj {
|
||||
copied := *obj
|
||||
nobj = &copied
|
||||
nobj.Count = 1
|
||||
}
|
||||
} else {
|
||||
p.PackUsed[obj.PackCh-'a'] = false
|
||||
detachObj(&p.Pack, obj)
|
||||
}
|
||||
|
||||
return nobj
|
||||
}
|
||||
|
||||
// attachMon pushes a monster onto the front of a list (list.c attach).
|
||||
func attachMon(list *[]*Monster, item *Monster) {
|
||||
*list = append([]*Monster{item}, *list...)
|
||||
}
|
||||
|
||||
// detachMon removes a monster (by identity) from a list (list.c detach).
|
||||
func detachMon(list *[]*Monster, item *Monster) {
|
||||
for i, m := range *list {
|
||||
if m == item {
|
||||
*list = append((*list)[:i], (*list)[i+1:]...)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
143
game/daemon.go
143
game/daemon.go
@@ -1,143 +0,0 @@
|
||||
package game
|
||||
|
||||
// daemon.c — the daemon/fuse scheduler.
|
||||
//
|
||||
// C identifies daemons and fuses by function pointer; the port uses DaemonID
|
||||
// (which the C save format itself resorted to when serializing d_list), and
|
||||
// dispatches through one switch in daemons.go.
|
||||
|
||||
// DaemonID names a daemon/fuse callback. The numeric values match the
|
||||
// function-pointer-to-int mapping in state.c rs_write_daemons.
|
||||
type DaemonID int
|
||||
|
||||
// Daemon and fuse callback identifiers. The first block's numeric values
|
||||
// match the function-pointer-to-int mapping in state.c rs_write_daemons;
|
||||
// the second block covers fuses state.c never saved (the Go save format
|
||||
// handles them all uniformly).
|
||||
const (
|
||||
DNone DaemonID = 0
|
||||
DRollwand DaemonID = 1
|
||||
DDoctor DaemonID = 2
|
||||
DStomach DaemonID = 3
|
||||
DRunners DaemonID = 4
|
||||
DSwander DaemonID = 5
|
||||
DNohaste DaemonID = 6
|
||||
DUnconfuse DaemonID = 7
|
||||
DUnsee DaemonID = 8
|
||||
DSight DaemonID = 9
|
||||
DVisuals DaemonID = 10
|
||||
DComeDown DaemonID = 11
|
||||
DLand DaemonID = 12
|
||||
DTurnSee DaemonID = 13 // potions.c casts turn_see to a fuse callback
|
||||
)
|
||||
|
||||
// Scheduling phases and slot states (daemon.c).
|
||||
const (
|
||||
dEmpty = 0
|
||||
Before = 1 // run before the player's command
|
||||
After = 2 // run after the player's command
|
||||
daemonTime = -1 // d_time sentinel: a recurring daemon, not a fuse
|
||||
)
|
||||
|
||||
// delayedAction is rogue.h struct delayed_action.
|
||||
type delayedAction struct {
|
||||
Type int // dEmpty, Before or After
|
||||
Func DaemonID
|
||||
Arg int
|
||||
Time int // daemonTime for daemons; remaining turns for fuses
|
||||
}
|
||||
|
||||
// DaemonList is the C d_list[MAXDAEMONS] plus the file statics that ride
|
||||
// along with the daemon system.
|
||||
type DaemonList struct {
|
||||
List [MaxDaemons]delayedAction
|
||||
Between int // daemons.c rollwand static `between`
|
||||
}
|
||||
|
||||
// dSlot finds an empty slot in the daemon/fuse list (daemon.c d_slot).
|
||||
func (g *RogueGame) dSlot() *delayedAction {
|
||||
for i := range g.Daemons.List {
|
||||
if g.Daemons.List[i].Type == dEmpty {
|
||||
return &g.Daemons.List[i]
|
||||
}
|
||||
}
|
||||
|
||||
panic("ran out of fuse slots") // C: debug message in MASTER, NULL deref otherwise
|
||||
}
|
||||
|
||||
// findSlot finds a particular slot in the table (daemon.c find_slot).
|
||||
func (g *RogueGame) findSlot(f DaemonID) *delayedAction {
|
||||
for i := range g.Daemons.List {
|
||||
d := &g.Daemons.List[i]
|
||||
if d.Type != dEmpty && d.Func == f {
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartDaemon starts a recurring daemon (daemon.c start_daemon).
|
||||
func (g *RogueGame) StartDaemon(f DaemonID, arg, typ int) {
|
||||
dev := g.dSlot()
|
||||
dev.Type = typ
|
||||
dev.Func = f
|
||||
dev.Arg = arg
|
||||
dev.Time = daemonTime
|
||||
}
|
||||
|
||||
// KillDaemon removes a daemon from the list (daemon.c kill_daemon).
|
||||
func (g *RogueGame) KillDaemon(f DaemonID) {
|
||||
if dev := g.findSlot(f); dev != nil {
|
||||
dev.Type = dEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// DoDaemons runs all the daemons that are active with the current flag
|
||||
// (daemon.c do_daemons).
|
||||
func (g *RogueGame) DoDaemons(flag int) {
|
||||
for i := range g.Daemons.List {
|
||||
dev := &g.Daemons.List[i]
|
||||
if dev.Type == flag && dev.Time == daemonTime {
|
||||
g.runDaemon(dev.Func, dev.Arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fuse starts a countdown fuse (daemon.c fuse).
|
||||
func (g *RogueGame) Fuse(f DaemonID, arg, time, typ int) {
|
||||
wire := g.dSlot()
|
||||
wire.Type = typ
|
||||
wire.Func = f
|
||||
wire.Arg = arg
|
||||
wire.Time = time
|
||||
}
|
||||
|
||||
// Lengthen extends a fuse's countdown (daemon.c lengthen).
|
||||
func (g *RogueGame) Lengthen(f DaemonID, xtime int) {
|
||||
if wire := g.findSlot(f); wire != nil {
|
||||
wire.Time += xtime
|
||||
}
|
||||
}
|
||||
|
||||
// Extinguish puts out a fuse (daemon.c extinguish).
|
||||
func (g *RogueGame) Extinguish(f DaemonID) {
|
||||
if wire := g.findSlot(f); wire != nil {
|
||||
wire.Type = dEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// DoFuses decrements all fuses in the given phase and fires any that burn
|
||||
// down (daemon.c do_fuses).
|
||||
func (g *RogueGame) DoFuses(flag int) {
|
||||
for i := range g.Daemons.List {
|
||||
wire := &g.Daemons.List[i]
|
||||
if wire.Type == flag && wire.Time > 0 {
|
||||
wire.Time--
|
||||
if wire.Time == 0 {
|
||||
wire.Type = dEmpty
|
||||
g.runDaemon(wire.Func, wire.Arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
282
game/daemons.go
282
game/daemons.go
@@ -1,282 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// daemons.c — the daemon and fuse callbacks, dispatched by DaemonID.
|
||||
// stomach() arrives with the endgame phase (starvation calls death()).
|
||||
|
||||
// runDaemon invokes the callback named by id (the call through d_func in C).
|
||||
func (g *RogueGame) runDaemon(id DaemonID, arg int) {
|
||||
h := g.data.daemonHandlers[id]
|
||||
if h == nil {
|
||||
// Handlers are added to the table as their subsystems are
|
||||
// ported; reaching one that isn't there is a porting bug.
|
||||
panic("daemon not yet ported")
|
||||
}
|
||||
|
||||
h(g, arg)
|
||||
}
|
||||
|
||||
// doctor is the healing daemon that restores hit points after rest
|
||||
// (daemons.c doctor).
|
||||
func (g *RogueGame) doctor(int) {
|
||||
p := &g.Player
|
||||
lv := p.Stats.Lvl
|
||||
ohp := p.Stats.HP
|
||||
|
||||
g.Quiet++
|
||||
if lv < 8 {
|
||||
if g.Quiet+(lv<<1) > 20 {
|
||||
p.Stats.HP++
|
||||
}
|
||||
} else if g.Quiet >= 3 {
|
||||
p.Stats.HP += g.rnd(lv-7) + 1
|
||||
}
|
||||
|
||||
if p.IsRing(Left, RingRegeneration) {
|
||||
p.Stats.HP++
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingRegeneration) {
|
||||
p.Stats.HP++
|
||||
}
|
||||
|
||||
if ohp != p.Stats.HP {
|
||||
if p.Stats.HP > p.Stats.MaxHP {
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.Quiet = 0
|
||||
}
|
||||
}
|
||||
|
||||
// swander is called when it is time to start rolling for wandering monsters
|
||||
// (daemons.c swander).
|
||||
func (g *RogueGame) swander(int) {
|
||||
g.StartDaemon(DRollwand, 0, Before)
|
||||
}
|
||||
|
||||
// rollwand rolls to see if a wandering monster starts up (daemons.c
|
||||
// rollwand).
|
||||
func (g *RogueGame) rollwand(int) {
|
||||
if g.Daemons.Between++; g.Daemons.Between >= 4 {
|
||||
if g.roll(1, 6) == 4 {
|
||||
g.wanderer()
|
||||
g.KillDaemon(DRollwand)
|
||||
g.Fuse(DSwander, 0, wanderTime(g), Before)
|
||||
}
|
||||
|
||||
g.Daemons.Between = 0
|
||||
}
|
||||
}
|
||||
|
||||
// wanderTime is the WANDERTIME macro: spread(70).
|
||||
func wanderTime(g *RogueGame) int { return g.spread(70) }
|
||||
|
||||
// unconfuse releases the poor player from his confusion (daemons.c
|
||||
// unconfuse).
|
||||
func (g *RogueGame) unconfuse(int) {
|
||||
g.Player.Flags.Clear(Confused)
|
||||
g.msg("you feel less %s now", g.chooseStr("trippy", "confused"))
|
||||
}
|
||||
|
||||
// unsee turns off the ability to see invisible (daemons.c unsee).
|
||||
func (g *RogueGame) unsee(int) {
|
||||
for _, th := range g.Level.Monsters {
|
||||
if th.On(Invisible) && g.seeMonst(th) {
|
||||
g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh)
|
||||
}
|
||||
}
|
||||
|
||||
g.Player.Flags.Clear(CanSeeInvisible)
|
||||
}
|
||||
|
||||
// sight gives the hero his sight back (daemons.c sight).
|
||||
func (g *RogueGame) sight(int) {
|
||||
p := &g.Player
|
||||
if p.On(Blind) {
|
||||
g.Extinguish(DSight)
|
||||
p.Flags.Clear(Blind)
|
||||
|
||||
if !p.Room.Flags.Has(Gone) {
|
||||
g.enterRoom(p.Pos)
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("far out! Everything is all cosmic again",
|
||||
"the veil of darkness lifts"))
|
||||
}
|
||||
}
|
||||
|
||||
// nohaste ends the hasting (daemons.c nohaste).
|
||||
func (g *RogueGame) nohaste(int) {
|
||||
g.Player.Flags.Clear(Hasted)
|
||||
g.msg("you feel yourself slowing down")
|
||||
}
|
||||
|
||||
// stomach digests the hero's food (daemons.c stomach).
|
||||
func (g *RogueGame) stomach(int) {
|
||||
p := &g.Player
|
||||
|
||||
origHungry := p.HungryState
|
||||
if p.FoodLeft <= 0 {
|
||||
g.stomachFaint()
|
||||
} else {
|
||||
g.stomachDigest()
|
||||
}
|
||||
|
||||
if p.HungryState != origHungry {
|
||||
p.Flags.Clear(Awake)
|
||||
|
||||
g.Running = false
|
||||
g.ToDeath = false
|
||||
g.Count = 0
|
||||
}
|
||||
}
|
||||
|
||||
// stomachFaint starves and possibly faints an empty-stomached hero (the
|
||||
// no-food arm of daemons.c stomach).
|
||||
func (g *RogueGame) stomachFaint() {
|
||||
p := &g.Player
|
||||
if p.FoodLeft--; p.FoodLeft < -StarveTime {
|
||||
g.death('s')
|
||||
}
|
||||
// the hero is fainting
|
||||
if g.NoCommand != 0 || g.rnd(5) != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
g.NoCommand += g.rnd(8) + 4
|
||||
p.HungryState = 3
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("%s", g.chooseStr(
|
||||
"the munchies overpower your motor capabilities. ",
|
||||
"you feel too weak from lack of food. "))
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("You freak out", "You faint"))
|
||||
}
|
||||
|
||||
// stomachDigest burns food and reports growing hunger (the fed arm of
|
||||
// daemons.c stomach).
|
||||
func (g *RogueGame) stomachDigest() {
|
||||
p := &g.Player
|
||||
oldfood := p.FoodLeft
|
||||
|
||||
amulet := 0
|
||||
if g.HasAmulet {
|
||||
amulet = 1
|
||||
}
|
||||
|
||||
p.FoodLeft -= g.ringEat(Left) + g.ringEat(Right) + 1 - amulet
|
||||
|
||||
if p.FoodLeft < MoreTime && oldfood >= MoreTime {
|
||||
p.HungryState = 2
|
||||
|
||||
g.msg("%s", g.chooseStr(
|
||||
"the munchies are interfering with your motor capabilities",
|
||||
"you are starting to feel weak"))
|
||||
} else if p.FoodLeft < 2*MoreTime && oldfood >= 2*MoreTime {
|
||||
p.HungryState = 1
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("%s", g.chooseStr("getting the munchies", "getting hungry"))
|
||||
} else {
|
||||
g.msg("%s", g.chooseStr("you are getting the munchies",
|
||||
"you are starting to get hungry"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// comeDown takes the hero down off her acid trip (daemons.c come_down).
|
||||
func (g *RogueGame) comeDown(int) {
|
||||
p := &g.Player
|
||||
if !p.On(Hallucinating) {
|
||||
return
|
||||
}
|
||||
|
||||
g.KillDaemon(DVisuals)
|
||||
p.Flags.Clear(Hallucinating)
|
||||
|
||||
if p.On(Blind) {
|
||||
return
|
||||
}
|
||||
|
||||
// undo the things
|
||||
for _, tp := range g.Level.Objects {
|
||||
if g.canSee(tp.Pos.Y, tp.Pos.X) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Kind.Glyph())
|
||||
}
|
||||
}
|
||||
|
||||
// undo the monsters
|
||||
seemonst := p.On(SenseMonsters)
|
||||
|
||||
for _, tp := range g.Level.Monsters {
|
||||
g.move(tp.Pos.Y, tp.Pos.X)
|
||||
|
||||
if g.canSee(tp.Pos.Y, tp.Pos.X) {
|
||||
if !tp.On(Invisible) || p.On(CanSeeInvisible) {
|
||||
g.addch(tp.Disguise)
|
||||
} else {
|
||||
g.addch(g.Level.Char(tp.Pos.Y, tp.Pos.X))
|
||||
}
|
||||
} else if seemonst {
|
||||
g.standout()
|
||||
g.addch(tp.Type)
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("Everything looks SO boring now.")
|
||||
}
|
||||
|
||||
// visuals changes the characters for the player while hallucinating
|
||||
// (daemons.c visuals).
|
||||
func (g *RogueGame) visuals(int) {
|
||||
if !g.After || (g.Running && g.Options.Jump) {
|
||||
return
|
||||
}
|
||||
// change the things
|
||||
for _, tp := range g.Level.Objects {
|
||||
if g.canSee(tp.Pos.Y, tp.Pos.X) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.rndThing())
|
||||
}
|
||||
}
|
||||
|
||||
// change the stairs
|
||||
if !g.SeenStairs && g.canSee(g.Level.Stairs.Y, g.Level.Stairs.X) {
|
||||
g.mvaddch(g.Level.Stairs.Y, g.Level.Stairs.X, g.rndThing())
|
||||
}
|
||||
|
||||
// change the monsters
|
||||
g.visualMonsters()
|
||||
}
|
||||
|
||||
// visualMonsters redraws the monsters through the hallucination (the
|
||||
// monster loop of daemons.c visuals).
|
||||
func (g *RogueGame) visualMonsters() {
|
||||
seemonst := g.Player.On(SenseMonsters)
|
||||
|
||||
for _, tp := range g.Level.Monsters {
|
||||
g.move(tp.Pos.Y, tp.Pos.X)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
if tp.Type == 'X' && tp.Disguise != 'X' {
|
||||
g.addch(g.rndThing())
|
||||
} else {
|
||||
g.addch(g.randomMonsterLetter())
|
||||
}
|
||||
} else if seemonst {
|
||||
g.standout()
|
||||
g.addch(g.randomMonsterLetter())
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// land lands the hero from a levitation potion (daemons.c land).
|
||||
func (g *RogueGame) land(int) {
|
||||
g.Player.Flags.Clear(Levitating)
|
||||
g.msg("%s", g.chooseStr("bummer! You've hit the ground",
|
||||
"you float gently to the ground"))
|
||||
}
|
||||
67
game/dice.go
67
game/dice.go
@@ -1,67 +0,0 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Rogue expresses damage as strings like "1x4/3x6": one attack rolling
|
||||
// 1d4 and a second rolling 3d6. The C code re-parsed these with atoi on
|
||||
// every swing (fight.c roll_em); the port parses them once, at table
|
||||
// definition or item creation, into a DiceSpec.
|
||||
|
||||
// DiceRoll is one attack's dice: Count rolls of a Sides-sided die.
|
||||
type DiceRoll struct {
|
||||
Count int
|
||||
Sides int
|
||||
}
|
||||
|
||||
// DiceSpec is the sequence of attacks a damage string described.
|
||||
type DiceSpec []DiceRoll
|
||||
|
||||
// ParseDice parses a C damage string with the exact semantics of the
|
||||
// roll_em loop: leading digits (C atoi) before and after each 'x', attacks
|
||||
// separated by '/'. Junk like the bestiary's "%%%x0" placeholder parses as
|
||||
// a single 0x0 attack, as it did in C.
|
||||
func ParseDice(s string) DiceSpec {
|
||||
var spec DiceSpec
|
||||
|
||||
for s != "" {
|
||||
count := cAtoi(s)
|
||||
|
||||
xi := strings.IndexByte(s, 'x')
|
||||
if xi < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
s = s[xi+1:]
|
||||
spec = append(spec, DiceRoll{Count: count, Sides: cAtoi(s)})
|
||||
|
||||
si := strings.IndexByte(s, '/')
|
||||
if si < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
s = s[si+1:]
|
||||
}
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
// dice is the table-definition shorthand for ParseDice.
|
||||
func dice(s string) DiceSpec { return ParseDice(s) }
|
||||
|
||||
// String renders the spec back in the classic "NxM/NxM" form.
|
||||
func (d DiceSpec) String() string {
|
||||
var sb strings.Builder
|
||||
|
||||
for i, r := range d {
|
||||
if i > 0 {
|
||||
sb.WriteByte('/')
|
||||
}
|
||||
|
||||
fmt.Fprintf(&sb, "%dx%d", r.Count, r.Sides)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// ParseDice must keep the exact semantics of the C roll_em parse loop,
|
||||
// including its junk-tolerant edges: the bestiary placeholder "%%%x0" and
|
||||
// the flytrap reset "000x0" both mean a single 0x0 attack.
|
||||
func TestParseDice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
in string
|
||||
want string
|
||||
len int
|
||||
}{
|
||||
{"1x4", "1x4", 1},
|
||||
{"1x2/1x5/1x5", "1x2/1x5/1x5", 3},
|
||||
{"2x12/2x4", "2x12/2x4", 2},
|
||||
{"0x0", "0x0", 1},
|
||||
{"000x0", "0x0", 1},
|
||||
{"%%%x0", "0x0", 1},
|
||||
{"", "", 0},
|
||||
{"3", "", 0}, // no 'x': C parses nothing
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := ParseDice(c.in)
|
||||
if len(got) != c.len || got.String() != c.want {
|
||||
t.Errorf("ParseDice(%q) = %v (len %d), want %q (len %d)",
|
||||
c.in, got, len(got), c.want, c.len)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The bestiary and weapon tables must parse to at least one attack each so
|
||||
// every creature and weapon actually swings.
|
||||
func TestTablesHaveDice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := newGameData()
|
||||
for i, m := range data.monsterTable {
|
||||
if len(m.Stats.Dmg) == 0 {
|
||||
t.Errorf("monster %c (%s) has no attacks", 'A'+i, m.Name)
|
||||
}
|
||||
}
|
||||
|
||||
for w, iw := range data.initWeaps {
|
||||
if len(iw.dam) == 0 || len(iw.hrl) == 0 {
|
||||
t.Errorf("weapon %d has empty dice", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// This file is the standing form of the issue #31 audit: every case
|
||||
// label in C's command switch against this port's dispatch. It exists
|
||||
// because a missing dispatch entry is the one porting error that leaves
|
||||
// no trace at build time. The port is function-by-function, so every C
|
||||
// function has a Go counterpart and a dropped key dangles nothing and
|
||||
// fails to compile nowhere; it simply answers "illegal command" the
|
||||
// first time a player presses it. That is how '+' (issue #11) survived
|
||||
// until PR #30 found it by accident.
|
||||
//
|
||||
// The tables below are transcribed from origin/c-master:command.c, with
|
||||
// the C line numbers alongside. Read them there with rogue.h 52-53 in
|
||||
// hand:
|
||||
//
|
||||
// #define when break;case
|
||||
// #define otherwise break;default
|
||||
//
|
||||
// The labels are therefore written "when 'x':", and a grep for "case "
|
||||
// finds ten of the eighty. CTRL is extern.h:113, (c & 037); ESCAPE is
|
||||
// rogue.h:121, 27.
|
||||
//
|
||||
// There are two switches and the split between them is load-bearing. A
|
||||
// key C answers from the main switch (151-427) must be answered here
|
||||
// whether or not wizard mode is on. A key C answers only from the
|
||||
// "if (wizard) switch (ch)" sub-switch (369-423) must not be reachable
|
||||
// outside it. '+' was a divergence in ordinary play, not just in wizard
|
||||
// mode, precisely because it is a main-switch key.
|
||||
//
|
||||
// The whole sub-switch, and '+' with it, is #ifdef MASTER. This port
|
||||
// targets the MASTER build: all four #ifdef MASTER sites in command.c
|
||||
// (67, 128, 317, 368) are ported unconditionally, as is sticks.c 237.
|
||||
|
||||
// cMainSwitchTableKeys are the main-switch labels whose arms are a plain
|
||||
// call, and which this port therefore answers from commandHandlers.
|
||||
func cMainSwitchTableKeys() []byte {
|
||||
return []byte{
|
||||
',', // 153
|
||||
'!', // 180
|
||||
'h', 'j', 'k', 'l', 'y', 'u', 'b', 'n', // 181-188 do_move
|
||||
'H', 'J', 'K', 'L', 'Y', 'U', 'B', 'N', // 189-196 do_run
|
||||
't', // 241
|
||||
'q', 'Q', 'i', 'I', 'd', 'r', 'e', 'w', // 258-269
|
||||
'W', 'T', 'P', 'R', 'o', 'c', // 270-275
|
||||
'>', '<', '?', '/', 's', 'z', 'D', // 276-286
|
||||
CTRL('P'), CTRL('R'), // 287-291
|
||||
'v', // 292
|
||||
'S', // 295
|
||||
'.', // 298 rest
|
||||
' ', // 299 "legal" illegal command
|
||||
'^', // 300
|
||||
'+', // 318 (#ifdef MASTER)
|
||||
Escape, // 339
|
||||
')', ']', '=', // 354-360
|
||||
'@', // 361
|
||||
}
|
||||
}
|
||||
|
||||
// cMainSwitchMultiStepKeys are the main-switch labels whose arms need
|
||||
// more than a call — C's "goto over" re-dispatch, or the F-to-f
|
||||
// fallthrough — and which this port therefore answers from dispatchKey's
|
||||
// own switch rather than from commandHandlers. They are main-switch keys
|
||||
// all the same, and a player reaches them without wizard mode.
|
||||
func cMainSwitchMultiStepKeys() []byte {
|
||||
return []byte{
|
||||
CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'), // 197
|
||||
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'), // 198
|
||||
'F', // 214 sets kamikaze, then falls through
|
||||
'f', // 217
|
||||
'a', // 246
|
||||
'm', // 344
|
||||
}
|
||||
}
|
||||
|
||||
// cWizardSwitchKeys are the labels of the "if (wizard) switch (ch)"
|
||||
// sub-switch, which sits inside the main switch's otherwise: arm.
|
||||
func cWizardSwitchKeys() []byte {
|
||||
return []byte{
|
||||
'|', // 371
|
||||
'C', // 372
|
||||
'$', // 373
|
||||
CTRL('G'), CTRL('W'), // 374-375
|
||||
CTRL('D'), CTRL('A'), // 376-377
|
||||
CTRL('F'), CTRL('T'), // 378-379
|
||||
CTRL('E'), CTRL('C'), // 380-381
|
||||
CTRL('X'), // 382
|
||||
CTRL('~'), // 383
|
||||
CTRL('I'), // 390
|
||||
'*', // 419
|
||||
}
|
||||
}
|
||||
|
||||
// assertNotIllegalCommand fails if the top line reports the key as
|
||||
// illegal. illcom is the only thing that writes that message, so it is a
|
||||
// reliable "the dispatch had no arm for this key" probe: it holds
|
||||
// whether the arm printed its own message, printed nothing, or cleared
|
||||
// the line on the way out.
|
||||
func assertNotIllegalCommand(t *testing.T, g *RogueGame, ch byte) {
|
||||
t.Helper()
|
||||
|
||||
// End() upper-cases the first letter, so match from the second.
|
||||
if line := g.scr.Std.Line(0); strings.Contains(line, "llegal command") {
|
||||
t.Errorf("dispatching '%s' reached illcom (top line %q); C answers "+
|
||||
"it from command.c's switch", unctrl(ch), strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommandHandlersMatchCMainSwitch pins commandHandlers to exactly the
|
||||
// set of main-switch keys C answers with a plain call. It is checked in
|
||||
// both directions on purpose. A missing key is the '+' bug. An extra key
|
||||
// is the same bug mirrored: the most likely way to acquire one is to
|
||||
// promote a key out of the wizard sub-switch, which would make a MASTER
|
||||
// debug command available in ordinary play.
|
||||
func TestCommandHandlersMatchCMainSwitch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handlers := newGameData().commandHandlers
|
||||
keys := cMainSwitchTableKeys()
|
||||
|
||||
want := make(map[byte]bool, len(keys))
|
||||
|
||||
for _, ch := range keys {
|
||||
want[ch] = true
|
||||
|
||||
if _, ok := handlers[ch]; !ok {
|
||||
t.Errorf("commandHandlers has no entry for '%s'; C answers it "+
|
||||
"from the main command.c switch, so this port says "+
|
||||
"\"illegal command\" where C does not", unctrl(ch))
|
||||
}
|
||||
}
|
||||
|
||||
for ch := range handlers {
|
||||
if !want[ch] {
|
||||
t.Errorf("commandHandlers has an entry for '%s' that C's main "+
|
||||
"switch does not; if C answers it only under if (wizard), "+
|
||||
"it belongs in wizardCommand", unctrl(ch))
|
||||
}
|
||||
}
|
||||
|
||||
if len(want) != len(keys) {
|
||||
t.Errorf("cMainSwitchTableKeys lists %d keys, %d of them distinct; "+
|
||||
"a duplicate hides a missing key", len(keys), len(want))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDispatchKeyAnswersCMultiStepKeys covers the main-switch keys that
|
||||
// commandHandlers cannot hold, which the set-equality test above cannot
|
||||
// see. Removing one of these from dispatchKey's switch is just as silent
|
||||
// as removing a map entry: it falls into the default arm and lands on
|
||||
// illcom, so that is what is checked.
|
||||
func TestDispatchKeyAnswersCMultiStepKeys(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, ch := range cMainSwitchMultiStepKeys() {
|
||||
t.Run(unctrl(ch), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
// Not a wizard: these are ordinary-play keys, so the default
|
||||
// arm they must not reach is illcom itself.
|
||||
g.Wizard = false
|
||||
g.Options.Terse = false
|
||||
// 'a' replays the last command; give it one to replay so it
|
||||
// takes its re-dispatch arm rather than its complaint arm.
|
||||
g.LastComm = '.'
|
||||
// F, f and m prompt for a direction. Escape backs out of the
|
||||
// prompt, which keeps them from moving the hero or starting a
|
||||
// fight while still proving their arm ran.
|
||||
setInput(t, g, Escape, Escape, Escape)
|
||||
|
||||
next, again := g.dispatchKey(ch)
|
||||
t.Logf("dispatchKey(%s) = %s, again=%v",
|
||||
unctrl(ch), unctrl(next), again)
|
||||
|
||||
assertNotIllegalCommand(t, g, ch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDispatchKeyRedispatchesCtrlDirections is the positive half of the
|
||||
// test above for the eight ctrl-directions: C's arm converts the key to
|
||||
// its upper-case run command and does "goto over" (command.c 197-213),
|
||||
// which this port spells as a true second result. Checking the returned
|
||||
// key, and not merely that illcom was missed, is what would catch the
|
||||
// arm being present but wired to the wrong direction.
|
||||
func TestDispatchKeyRedispatchesCtrlDirections(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, ch := range []byte{
|
||||
CTRL('H'), CTRL('J'), CTRL('K'), CTRL('L'),
|
||||
CTRL('Y'), CTRL('U'), CTRL('B'), CTRL('N'),
|
||||
} {
|
||||
t.Run(unctrl(ch), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
|
||||
// C's "ch += ('A' - CTRL('A'))": ctrl-h becomes 'H'.
|
||||
wantCh := ch + 'A' - CTRL('A')
|
||||
|
||||
next, again := g.dispatchKey(ch)
|
||||
if !again {
|
||||
t.Fatalf("dispatchKey(%s) did not ask to re-dispatch; C's "+
|
||||
"arm ends in goto over", unctrl(ch))
|
||||
}
|
||||
|
||||
if next != wantCh {
|
||||
t.Errorf("dispatchKey(%s) re-dispatched as %q, want %q",
|
||||
unctrl(ch), next, wantCh)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWizardDispatchAnswersCWizardSwitch is the same guard for the
|
||||
// MASTER sub-switch, driven through dispatchKey rather than through
|
||||
// wizardCommand directly so that the routing is covered too: these keys
|
||||
// must be answered because wizard mode is on, not because they leaked
|
||||
// into commandHandlers.
|
||||
func TestWizardDispatchAnswersCWizardSwitch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, ch := range cWizardSwitchKeys() {
|
||||
t.Run(unctrl(ch), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
g.Wizard = true
|
||||
// ctrl-a is "level--; new_level()", so start deep enough for
|
||||
// it to have somewhere to go.
|
||||
g.Depth = 5
|
||||
// Escape backs out of the item and type prompts that ctrl-w,
|
||||
// ctrl-~, 'C' and '*' put up. testTerm keeps answering after
|
||||
// the script runs out, so nothing here can block.
|
||||
setInput(t, g, Escape, Escape, Escape, Escape)
|
||||
|
||||
g.dispatchKey(ch)
|
||||
|
||||
assertNotIllegalCommand(t, g, ch)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// mkGameInput builds a headless game; tests script it via setInput. The
|
||||
// fixed seed keeps the scripted item/monster interactions stable.
|
||||
func mkGameInput(t *testing.T) *RogueGame {
|
||||
t.Helper()
|
||||
|
||||
g := New(Params{Seed: 5, Term: &testTerm{}})
|
||||
g.NewLevel()
|
||||
g.Oldpos = g.Player.Pos
|
||||
g.Oldrp = g.roomIn(g.Player.Pos)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// give puts an object straight into the pack and returns its pack letter.
|
||||
func give(g *RogueGame, obj *Object) byte {
|
||||
obj.Count = 1
|
||||
g.addPack(obj, true)
|
||||
|
||||
return obj.PackCh
|
||||
}
|
||||
|
||||
// setInput replaces the scripted terminal input.
|
||||
func setInput(t *testing.T, g *RogueGame, input ...byte) {
|
||||
t.Helper()
|
||||
|
||||
tt, ok := g.scr.term.(*testTerm)
|
||||
if !ok {
|
||||
t.Fatal("game terminal is not a testTerm")
|
||||
}
|
||||
|
||||
tt.input = input
|
||||
tt.pos = 0
|
||||
}
|
||||
|
||||
func TestQuaffHealingPotion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
pot := newObject()
|
||||
pot.Kind = KindPotion
|
||||
pot.Which = int(PotionHealing)
|
||||
ch := give(g, pot)
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.Player.Stats.HP = 1
|
||||
g.quaff()
|
||||
|
||||
if g.Player.Stats.HP <= 1 {
|
||||
t.Error("healing potion did not heal")
|
||||
}
|
||||
|
||||
if !g.Items.Potions[PotionHealing].Know {
|
||||
t.Error("healing potion not identified after drinking")
|
||||
}
|
||||
|
||||
if len(g.Player.Pack) != 5 {
|
||||
t.Errorf("potion not consumed: %d items", len(g.Player.Pack))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuaffConfusionSetsFlagAndFuse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
pot := newObject()
|
||||
pot.Kind = KindPotion
|
||||
pot.Which = int(PotionConfusion)
|
||||
ch := give(g, pot)
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.quaff()
|
||||
|
||||
if !g.Player.On(Confused) {
|
||||
t.Error("confusion potion did not confuse")
|
||||
}
|
||||
|
||||
if g.findSlot(DUnconfuse) == nil {
|
||||
t.Error("no unconfuse fuse pending")
|
||||
}
|
||||
// Let the fuse burn down
|
||||
for range 30 {
|
||||
g.DoFuses(After)
|
||||
}
|
||||
|
||||
if g.Player.On(Confused) {
|
||||
t.Error("confusion never wore off")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadEnchantArmor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
scr := newObject()
|
||||
scr.Kind = KindScroll
|
||||
scr.Which = int(ScrollEnchantArmor)
|
||||
ch := give(g, scr)
|
||||
setInput(t, g, ch)
|
||||
|
||||
before := g.Player.CurArmor.ArmorClass
|
||||
g.readScroll()
|
||||
|
||||
if g.Player.CurArmor.ArmorClass != before-1 {
|
||||
t.Errorf("enchant armor: AC %d -> %d, want %d",
|
||||
before, g.Player.CurArmor.ArmorClass, before-1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadHoldMonsterFreezesAdjacent(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Note: this must not use a greedy monster ('O' orc, ISGREED): the C
|
||||
// wake_monster gold-guarding check has no ISHELD guard, so the
|
||||
// look(TRUE) at the end of read_scroll immediately re-wakes greedy
|
||||
// monsters. The port reproduces that quirk faithfully — see
|
||||
// TestHoldScrollGreedyMonsterQuirk.
|
||||
g := mkGameInput(t)
|
||||
tp := spawnAdjacent(g, 'Z')
|
||||
tp.Flags.Set(Awake)
|
||||
|
||||
scr := newObject()
|
||||
scr.Kind = KindScroll
|
||||
scr.Which = int(ScrollHoldMonster)
|
||||
ch := give(g, scr)
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.readScroll()
|
||||
t.Logf("after scroll: flags=%o huh=%q", tp.Flags, g.Msgs.Huh)
|
||||
|
||||
if tp.On(Awake) || !tp.On(Held) {
|
||||
t.Error("hold monster scroll did not hold the adjacent monster")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHoldScrollGreedyMonsterQuirk documents a C behavior the port keeps:
|
||||
// wake_monster's ISGREED branch lacks an ISHELD guard, so a greedy monster
|
||||
// (orc) held by a scroll is re-woken by the look(TRUE) that read_scroll
|
||||
// performs, ending up both held and running again.
|
||||
func TestHoldScrollGreedyMonsterQuirk(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
tp := spawnAdjacent(g, 'O')
|
||||
tp.Flags.Set(Awake)
|
||||
|
||||
scr := newObject()
|
||||
scr.Kind = KindScroll
|
||||
scr.Which = int(ScrollHoldMonster)
|
||||
ch := give(g, scr)
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.readScroll()
|
||||
t.Logf("orc after scroll: flags=%o (Awake=%v Held=%v)",
|
||||
tp.Flags, tp.On(Awake), tp.On(Held))
|
||||
|
||||
if !tp.On(Held) {
|
||||
t.Error("orc lost Held entirely")
|
||||
}
|
||||
|
||||
if !tp.On(Awake) {
|
||||
t.Error("quirk changed: greedy monster stayed held; if this is a " +
|
||||
"deliberate fix, update this test and ARCHITECTURE.md")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZapSlowMonster(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
tp := spawnAdjacent(g, 'Z')
|
||||
stick := newObject()
|
||||
stick.Kind = KindWand
|
||||
stick.Which = int(WandSlowMonster)
|
||||
g.fixStick(stick)
|
||||
ch := give(g, stick)
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.Delta = Coord{X: 1, Y: 0} // aim at the monster
|
||||
|
||||
charges := stick.Charges
|
||||
|
||||
g.doZap()
|
||||
|
||||
if !tp.On(Slowed) {
|
||||
t.Error("slow monster wand did not slow")
|
||||
}
|
||||
|
||||
if stick.Charges != charges-1 {
|
||||
t.Error("zap did not use a charge")
|
||||
}
|
||||
}
|
||||
|
||||
// bizarreSchtick is C's message for a zap that matched no case at all
|
||||
// (sticks.c do_zap, the "otherwise" arm). Shared by the pair of tests
|
||||
// below so that the one asserting it appears and the one asserting it
|
||||
// does not can never drift apart.
|
||||
const bizarreSchtick = "what a bizarre schtick!"
|
||||
|
||||
// TestZapUnhandledWandSaysBizarreSchtick pins the closing arm of C's zap
|
||||
// switch. Every WS_ kind has a case, so the arm is reachable only for an
|
||||
// o_which outside the table — here a wand one past the end, the state a
|
||||
// corrupt save file can still describe. C's message is not gated on the
|
||||
// wizard flag, only on the MASTER build this port is, so no test setup
|
||||
// turns it on.
|
||||
func TestZapUnhandledWandSaysBizarreSchtick(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
wand := malformed(KindWand)
|
||||
wand.Charges = 3
|
||||
ch := give(g, wand)
|
||||
|
||||
setInput(t, g, ch)
|
||||
g.Msgs.Huh = ""
|
||||
|
||||
g.doZap()
|
||||
|
||||
if g.Msgs.Huh != bizarreSchtick {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, bizarreSchtick)
|
||||
}
|
||||
|
||||
// C falls out of the switch into o_charges-- from the otherwise arm
|
||||
// as much as from any other.
|
||||
if wand.Charges != 2 {
|
||||
t.Errorf("charges = %d after zapping, want 2", wand.Charges)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapWandOfNothingIsSilent is the other half, and the reason the
|
||||
// message cannot simply be attached to "no handler ran". WS_NOP is a case
|
||||
// of C's switch in its own right — "when WS_NOP: break;" — so the wand
|
||||
// that does nothing does it quietly, and only a kind C had no case for
|
||||
// is bizarre.
|
||||
func TestZapWandOfNothingIsSilent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
stick := newObject()
|
||||
stick.Kind = KindWand
|
||||
stick.Which = int(WandNothing)
|
||||
g.fixStick(stick)
|
||||
ch := give(g, stick)
|
||||
|
||||
setInput(t, g, ch)
|
||||
|
||||
charges := stick.Charges
|
||||
g.Msgs.Huh = ""
|
||||
|
||||
g.doZap()
|
||||
|
||||
if g.Msgs.Huh == bizarreSchtick {
|
||||
t.Errorf("the wand of nothing said %q; WS_NOP is a case of C's "+
|
||||
"switch, not an unhandled kind", bizarreSchtick)
|
||||
}
|
||||
|
||||
if stick.Charges != charges-1 {
|
||||
t.Error("zap did not use a charge")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := New(Params{Seed: 1})
|
||||
g.ParseOpts("terse,nojump,name=Conan,fruit=mango,inven=slow")
|
||||
|
||||
if !g.Options.Terse {
|
||||
t.Error("terse not set")
|
||||
}
|
||||
|
||||
if g.Options.Jump {
|
||||
t.Error("nojump not honored")
|
||||
}
|
||||
|
||||
if g.Whoami != "Conan" {
|
||||
t.Errorf("name = %q", g.Whoami)
|
||||
}
|
||||
|
||||
if g.Fruit != "mango" {
|
||||
t.Errorf("fruit = %q", g.Fruit)
|
||||
}
|
||||
|
||||
if g.Options.InvType != InvSlow {
|
||||
t.Errorf("inven = %d", g.Options.InvType)
|
||||
}
|
||||
}
|
||||
735
game/fight.go
735
game/fight.go
@@ -1,735 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "strconv"
|
||||
|
||||
// fight.c — all the fighting gets done here.
|
||||
|
||||
// setMname returns the monster name for the given monster (fight.c
|
||||
// set_mname).
|
||||
func (g *RogueGame) setMname(tp *Monster) string {
|
||||
if !g.seeMonst(tp) && !g.Player.On(SenseMonsters) {
|
||||
if g.Options.Terse {
|
||||
return "it"
|
||||
}
|
||||
|
||||
return "something"
|
||||
}
|
||||
|
||||
var mname string
|
||||
|
||||
if g.Player.On(Hallucinating) {
|
||||
var idx int
|
||||
|
||||
ch := g.mvinch(tp.Pos.Y, tp.Pos.X)
|
||||
if isUpper(ch) {
|
||||
idx = int(ch - 'A')
|
||||
} else {
|
||||
idx = g.rnd(26)
|
||||
}
|
||||
|
||||
mname = g.Monsters[idx].Name
|
||||
} else {
|
||||
mname = g.Monsters[tp.Type-'A'].Name
|
||||
}
|
||||
|
||||
return "the " + mname
|
||||
}
|
||||
|
||||
// fight has the player attack the monster at mp (fight.c fight).
|
||||
func (g *RogueGame) fight(mp Coord, weap *Object, thrown bool) bool {
|
||||
p := &g.Player
|
||||
// Find the monster we want to fight
|
||||
tp := g.Level.MonsterAt(mp.Y, mp.X)
|
||||
if tp == nil {
|
||||
return false
|
||||
}
|
||||
// Since we are fighting, things are not quiet so no healing takes
|
||||
// place.
|
||||
g.Count = 0
|
||||
g.Quiet = 0
|
||||
g.runTo(mp)
|
||||
|
||||
if g.revealXeroc(tp) && !thrown {
|
||||
return false
|
||||
}
|
||||
|
||||
mname := g.setMname(tp)
|
||||
|
||||
g.HasHit = g.Options.Terse && !g.ToDeath
|
||||
if g.rollAttacks(&p.Creature, &tp.Creature, weap, thrown) {
|
||||
g.heroHits(tp, mname, weap, thrown)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if thrown {
|
||||
g.bounce(weap, mname, g.Options.Terse)
|
||||
} else {
|
||||
g.miss("", mname, g.Options.Terse)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// revealXeroc lets him know it was really a xeroc (if it was one); it
|
||||
// reports whether one was unmasked (the X block of fight.c fight).
|
||||
func (g *RogueGame) revealXeroc(tp *Monster) bool {
|
||||
p := &g.Player
|
||||
if tp.Type != 'X' || tp.Disguise == 'X' || p.On(Blind) {
|
||||
return false
|
||||
}
|
||||
|
||||
tp.Disguise = 'X'
|
||||
if p.On(Hallucinating) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
|
||||
"wait! That's a xeroc!"))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// heroHits lands the hero's blow on a monster: messages, the confusing
|
||||
// touch, and the kill check (the hit arm of fight.c fight).
|
||||
func (g *RogueGame) heroHits(tp *Monster, mname string, weap *Object, thrown bool) {
|
||||
p := &g.Player
|
||||
confused := false
|
||||
|
||||
if thrown {
|
||||
g.thunk(weap, mname, g.Options.Terse)
|
||||
} else {
|
||||
g.hit("", mname, g.Options.Terse)
|
||||
}
|
||||
|
||||
if p.On(CanConfuse) {
|
||||
confused = true
|
||||
|
||||
tp.Flags.Set(Confused)
|
||||
p.Flags.Clear(CanConfuse)
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
g.msg("your hands stop glowing %s", g.pickColor("red"))
|
||||
}
|
||||
|
||||
if tp.Stats.HP <= 0 {
|
||||
g.killed(tp, true)
|
||||
} else if confused && !p.On(Blind) {
|
||||
g.msg("%s appears confused", mname)
|
||||
}
|
||||
}
|
||||
|
||||
// attack has the monster attack the player (fight.c attack). The result
|
||||
// reports that the monster took itself off the level during its own
|
||||
// attack (the C -1 return).
|
||||
func (g *RogueGame) attack(mp *Monster) bool {
|
||||
p := &g.Player
|
||||
// Since this is an attack, stop running and any healing that was
|
||||
// going on at the time.
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
|
||||
g.Quiet = 0
|
||||
if g.ToDeath && !mp.On(Targeted) {
|
||||
g.ToDeath = false
|
||||
g.Kamikaze = false
|
||||
}
|
||||
|
||||
if mp.Type == 'X' && mp.Disguise != 'X' && !p.On(Blind) {
|
||||
mp.Disguise = 'X'
|
||||
if p.On(Hallucinating) {
|
||||
g.mvaddch(mp.Pos.Y, mp.Pos.X, g.randomMonsterLetter())
|
||||
}
|
||||
}
|
||||
|
||||
mname := g.setMname(mp)
|
||||
oldhp := p.Stats.HP
|
||||
removed := false
|
||||
|
||||
if g.rollAttacks(&mp.Creature, &p.Creature, nil, false) {
|
||||
removed = g.monsterHit(mp, mname, oldhp)
|
||||
} else {
|
||||
g.monsterMiss(mp, mname)
|
||||
}
|
||||
|
||||
if g.Options.FightFlush && !g.ToDeath {
|
||||
g.flushType()
|
||||
}
|
||||
|
||||
g.Count = 0
|
||||
g.status()
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
// monsterHit lands a monster's blow on the hero: messages, death and
|
||||
// to-death bookkeeping, then the monster's special power (the hit arm
|
||||
// of fight.c attack). It reports whether the monster removed itself.
|
||||
func (g *RogueGame) monsterHit(mp *Monster, mname string, oldhp int) bool {
|
||||
p := &g.Player
|
||||
if mp.Type != 'I' {
|
||||
if g.HasHit {
|
||||
g.addmsgf(". ")
|
||||
}
|
||||
|
||||
g.hit(mname, "", false)
|
||||
} else if g.HasHit {
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.HasHit = false
|
||||
if p.Stats.HP <= 0 {
|
||||
g.death(mp.Type) // Bye bye life ...
|
||||
} else if !g.Kamikaze {
|
||||
oldhp -= p.Stats.HP
|
||||
if oldhp > g.MaxHit {
|
||||
g.MaxHit = oldhp
|
||||
}
|
||||
|
||||
if p.Stats.HP <= g.MaxHit {
|
||||
g.ToDeath = false
|
||||
}
|
||||
}
|
||||
|
||||
if !mp.On(Cancelled) {
|
||||
if h := g.data.hitHandlers[mp.Type-'A']; h != nil {
|
||||
return h(g, mp, mname)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// monsterMiss handles a monster's whiffed swing (the miss arm of
|
||||
// fight.c attack); ice monsters miss silently.
|
||||
func (g *RogueGame) monsterMiss(mp *Monster, mname string) {
|
||||
if mp.Type == 'I' {
|
||||
return
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
|
||||
if g.HasHit {
|
||||
g.addmsgf(". ")
|
||||
g.HasHit = false
|
||||
}
|
||||
|
||||
if mp.Type == 'F' {
|
||||
p.Stats.HP -= p.VfHit
|
||||
if p.Stats.HP <= 0 {
|
||||
g.death(mp.Type) // Bye bye life ...
|
||||
}
|
||||
}
|
||||
|
||||
g.miss(mname, "", false)
|
||||
}
|
||||
|
||||
// The monster special-power handlers, dispatched through
|
||||
// gameData.hitHandlers when an uncancelled monster's hit lands. Each is
|
||||
// one case of the C attack switch; a true return means the monster
|
||||
// removed itself from the level.
|
||||
|
||||
func (g *RogueGame) hitAquator(*Monster, string) bool {
|
||||
// If an aquator hits, you can lose armor class.
|
||||
g.rustArmor(g.Player.CurArmor)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitIceMonster(_ *Monster, mname string) bool {
|
||||
// The ice monster freezes you
|
||||
g.Player.Flags.Clear(Awake)
|
||||
|
||||
if g.NoCommand == 0 {
|
||||
g.addmsgf("you are frozen")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" by the %s", mname)
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
g.NoCommand += g.rnd(2) + 2
|
||||
if g.NoCommand > BoreLevel {
|
||||
g.death('h')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitRattlesnake(*Monster, string) bool {
|
||||
// Rattlesnakes have poisonous bites
|
||||
if g.save(VsPoison) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !g.Player.IsWearing(RingSustainStrength) {
|
||||
g.changeStrength(-1)
|
||||
g.msg("%s", g.chooseTerse("a bite has weakened you",
|
||||
"you feel a bite in your leg and now feel weaker"))
|
||||
} else if !g.ToDeath {
|
||||
g.msg("%s", g.chooseTerse("bite has no effect",
|
||||
"a bite momentarily weakens you"))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitLifeDrainer(mp *Monster, _ string) bool {
|
||||
// Wraiths might drain energy levels, and Vampires can steal max_hp
|
||||
p := &g.Player
|
||||
|
||||
chance := 30
|
||||
if mp.Type == 'W' {
|
||||
chance = 15
|
||||
}
|
||||
|
||||
if g.rnd(100) >= chance {
|
||||
return false
|
||||
}
|
||||
|
||||
var fewer int
|
||||
|
||||
if mp.Type == 'W' {
|
||||
if p.Stats.Exp == 0 {
|
||||
g.death('W') // All levels gone
|
||||
}
|
||||
|
||||
if p.Stats.Lvl--; p.Stats.Lvl == 0 {
|
||||
p.Stats.Exp = 0
|
||||
p.Stats.Lvl = 1
|
||||
} else {
|
||||
p.Stats.Exp = g.data.eLevels[p.Stats.Lvl-1] + 1
|
||||
}
|
||||
|
||||
fewer = g.roll(1, 10)
|
||||
} else {
|
||||
fewer = g.roll(1, 3)
|
||||
}
|
||||
|
||||
p.Stats.HP -= fewer
|
||||
|
||||
p.Stats.MaxHP -= fewer
|
||||
if p.Stats.HP <= 0 {
|
||||
p.Stats.HP = 1
|
||||
}
|
||||
|
||||
if p.Stats.MaxHP <= 0 {
|
||||
g.death(mp.Type)
|
||||
}
|
||||
|
||||
g.msg("you suddenly feel weaker")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitFlytrap(*Monster, string) bool {
|
||||
// Venus Flytrap stops the poor guy from moving
|
||||
p := &g.Player
|
||||
p.Flags.Set(Held)
|
||||
p.VfHit++
|
||||
|
||||
g.Monsters['F'-'A'].Stats.Dmg = DiceSpec{{Count: p.VfHit, Sides: 1}}
|
||||
if p.Stats.HP--; p.Stats.HP <= 0 {
|
||||
g.death('F')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitLeprechaun(mp *Monster, _ string) bool {
|
||||
// Leprechaun steals some gold
|
||||
p := &g.Player
|
||||
lastpurse := p.Purse
|
||||
|
||||
p.Purse -= g.goldCalc()
|
||||
if !g.save(VsMagic) {
|
||||
p.Purse -= g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
|
||||
}
|
||||
|
||||
if p.Purse < 0 {
|
||||
p.Purse = 0
|
||||
}
|
||||
|
||||
g.removeMon(mp.Pos, mp, false)
|
||||
|
||||
if p.Purse != lastpurse {
|
||||
g.msg("your purse feels lighter")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) hitNymph(mp *Monster, _ string) bool {
|
||||
// Nymphs steal a magic item; look through the pack and pick out one
|
||||
// we like.
|
||||
p := &g.Player
|
||||
|
||||
var steal *Object
|
||||
|
||||
nobj := 0
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
if obj != p.CurArmor && obj != p.CurWeapon &&
|
||||
obj != p.CurRing[Left] && obj != p.CurRing[Right] &&
|
||||
g.isMagic(obj) {
|
||||
if nobj++; g.rnd(nobj) == 0 {
|
||||
steal = obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if steal == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
|
||||
g.leavePack(steal, false, false)
|
||||
g.msg("she stole %s!", g.inventoryName(steal, true))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// swing returns true if the swing hits (fight.c swing).
|
||||
func (g *RogueGame) swing(atLvl, opArm, wplus int) bool {
|
||||
res := g.rnd(20)
|
||||
need := (20 - atLvl) - opArm
|
||||
|
||||
return res+wplus >= need
|
||||
}
|
||||
|
||||
// rollAttacks rolls several attacks (fight.c roll_em).
|
||||
func (g *RogueGame) rollAttacks(thatt, thdef *Creature, weap *Object, hurl bool) bool {
|
||||
att := &thatt.Stats
|
||||
def := &thdef.Stats
|
||||
|
||||
var (
|
||||
attacks DiceSpec
|
||||
hplus, dplus int
|
||||
)
|
||||
|
||||
if weap == nil {
|
||||
attacks = att.Dmg
|
||||
} else {
|
||||
attacks, hplus, dplus = g.weaponAttack(weap, hurl)
|
||||
}
|
||||
// If the creature being attacked is not running (asleep or held) then
|
||||
// the attacker gets a plus four bonus to hit.
|
||||
if !thdef.Flags.Has(Awake) {
|
||||
hplus += 4
|
||||
}
|
||||
|
||||
defArm := g.defenderArmor(thdef)
|
||||
didHit := false
|
||||
|
||||
for _, atk := range attacks {
|
||||
if g.swing(att.Lvl, defArm, hplus+g.data.strPlus[att.Str]) {
|
||||
proll := g.roll(atk.Count, atk.Sides)
|
||||
|
||||
damage := dplus + proll + g.data.addDam[att.Str]
|
||||
if damage > 0 {
|
||||
def.HP -= damage
|
||||
}
|
||||
|
||||
didHit = true
|
||||
}
|
||||
}
|
||||
|
||||
return didHit
|
||||
}
|
||||
|
||||
// weaponAttack picks the dice and to-hit/damage bonuses a weapon swings
|
||||
// with: ring bonuses when wielded, and launcher pairing for hurled
|
||||
// missiles (the weapon preamble of fight.c roll_em).
|
||||
func (g *RogueGame) weaponAttack(weap *Object, hurl bool) (DiceSpec, int, int) {
|
||||
p := &g.Player
|
||||
|
||||
hplus := weap.HPlus
|
||||
|
||||
dplus := weap.DPlus
|
||||
if weap == p.CurWeapon {
|
||||
hplus, dplus = g.wieldedRingBonus(hplus, dplus)
|
||||
}
|
||||
|
||||
attacks := weap.Damage
|
||||
if hurl {
|
||||
if weap.Flags.Has(Missile) && p.CurWeapon != nil &&
|
||||
WeaponKind(p.CurWeapon.Which) == weap.Launch {
|
||||
attacks = weap.HurlDmg
|
||||
hplus += p.CurWeapon.HPlus
|
||||
dplus += p.CurWeapon.DPlus
|
||||
} else if weap.Launch < 0 {
|
||||
attacks = weap.HurlDmg
|
||||
}
|
||||
}
|
||||
|
||||
return attacks, hplus, dplus
|
||||
}
|
||||
|
||||
// wieldedRingBonus folds damage and dexterity ring bonuses into the
|
||||
// wielded weapon's to-hit/damage pluses (fight.c roll_em).
|
||||
func (g *RogueGame) wieldedRingBonus(hplus, dplus int) (int, int) {
|
||||
p := &g.Player
|
||||
if p.IsRing(Left, RingIncreaseDamage) {
|
||||
dplus += p.CurRing[Left].Bonus
|
||||
} else if p.IsRing(Left, RingDexterity) {
|
||||
hplus += p.CurRing[Left].Bonus
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingIncreaseDamage) {
|
||||
dplus += p.CurRing[Right].Bonus
|
||||
} else if p.IsRing(Right, RingDexterity) {
|
||||
hplus += p.CurRing[Right].Bonus
|
||||
}
|
||||
|
||||
return hplus, dplus
|
||||
}
|
||||
|
||||
// defenderArmor computes the defender's effective armor class: worn
|
||||
// armor and protection rings when the hero defends (the def_arm
|
||||
// computation of fight.c roll_em).
|
||||
func (g *RogueGame) defenderArmor(thdef *Creature) int {
|
||||
p := &g.Player
|
||||
def := &thdef.Stats
|
||||
|
||||
defArm := def.ArmorClass
|
||||
if def == &p.Stats {
|
||||
if p.CurArmor != nil {
|
||||
defArm = p.CurArmor.ArmorClass
|
||||
}
|
||||
|
||||
if p.IsRing(Left, RingProtection) {
|
||||
defArm -= p.CurRing[Left].Bonus
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingProtection) {
|
||||
defArm -= p.CurRing[Right].Bonus
|
||||
}
|
||||
}
|
||||
|
||||
return defArm
|
||||
}
|
||||
|
||||
// cAtoi parses a leading integer like C atoi: trailing non-digits are
|
||||
// ignored rather than an error.
|
||||
func cAtoi(s string) int {
|
||||
i := 0
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
|
||||
n, _ := strconv.Atoi(s[:i])
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// prname gives the print name of a combatant; "" is the player (fight.c
|
||||
// prname).
|
||||
func prname(mname string, upper bool) string {
|
||||
out := mname
|
||||
if out == "" {
|
||||
out = "you"
|
||||
}
|
||||
|
||||
if upper {
|
||||
out = string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// thunk announces that a missile hit a monster (fight.c thunk).
|
||||
func (g *RogueGame) thunk(weap *Object, mname string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
|
||||
if weap.Kind == KindWeapon {
|
||||
g.addmsgf("the %s hits ", g.Items.Weapons[weap.Which].Name)
|
||||
} else {
|
||||
g.addmsgf("you hit ")
|
||||
}
|
||||
|
||||
g.addmsgf("%s", mname)
|
||||
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
|
||||
// hit prints a message to indicate a successful hit (fight.c hit).
|
||||
func (g *RogueGame) hit(er, ee string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
|
||||
g.addmsgf("%s", prname(er, true))
|
||||
|
||||
var s string
|
||||
if g.Options.Terse {
|
||||
s = " hit"
|
||||
} else {
|
||||
i := g.rnd(4)
|
||||
if er != "" {
|
||||
i += 4
|
||||
}
|
||||
|
||||
s = g.data.hNames[i]
|
||||
}
|
||||
|
||||
g.addmsgf("%s", s)
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("%s", prname(ee, false))
|
||||
}
|
||||
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
|
||||
// miss prints a message to indicate a poor swing (fight.c miss).
|
||||
func (g *RogueGame) miss(er, ee string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
|
||||
g.addmsgf("%s", prname(er, true))
|
||||
|
||||
i := 0
|
||||
if !g.Options.Terse {
|
||||
i = g.rnd(4)
|
||||
}
|
||||
|
||||
if er != "" {
|
||||
i += 4
|
||||
}
|
||||
|
||||
g.addmsgf("%s", g.data.mNames[i])
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" %s", prname(ee, false))
|
||||
}
|
||||
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
|
||||
// bounce announces that a missile missed a monster (fight.c bounce).
|
||||
func (g *RogueGame) bounce(weap *Object, mname string, noend bool) {
|
||||
if g.ToDeath {
|
||||
return
|
||||
}
|
||||
|
||||
if weap.Kind == KindWeapon {
|
||||
g.addmsgf("the %s misses ", g.Items.Weapons[weap.Which].Name)
|
||||
} else {
|
||||
g.addmsgf("you missed ")
|
||||
}
|
||||
|
||||
g.addmsgf("%s", mname)
|
||||
|
||||
if !noend {
|
||||
g.endmsg()
|
||||
}
|
||||
}
|
||||
|
||||
// removeMon removes a monster from the screen (fight.c remove_mon).
|
||||
func (g *RogueGame) removeMon(mp Coord, tp *Monster, waskill bool) {
|
||||
pack := append([]*Object(nil), tp.Pack...)
|
||||
for _, obj := range pack {
|
||||
obj.Pos = tp.Pos
|
||||
detachObj(&tp.Pack, obj)
|
||||
|
||||
if waskill {
|
||||
g.fall(obj, false)
|
||||
}
|
||||
}
|
||||
|
||||
g.Level.SetMonsterAt(mp.Y, mp.X, nil)
|
||||
g.mvaddch(mp.Y, mp.X, tp.OldCh)
|
||||
g.Level.RemoveMonster(tp)
|
||||
|
||||
if tp.On(Targeted) {
|
||||
g.Kamikaze = false
|
||||
|
||||
g.ToDeath = false
|
||||
if g.Options.FightFlush {
|
||||
g.flushType()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// killed is called to put a monster to death (fight.c killed).
|
||||
func (g *RogueGame) killed(tp *Monster, pr bool) {
|
||||
p := &g.Player
|
||||
p.Stats.Exp += tp.Stats.Exp
|
||||
|
||||
g.killedSpecial(tp)
|
||||
// Get rid of the monster.
|
||||
mname := g.setMname(tp)
|
||||
g.removeMon(tp.Pos, tp, true)
|
||||
|
||||
if pr {
|
||||
if g.HasHit {
|
||||
g.addmsgf(". Defeated ")
|
||||
g.HasHit = false
|
||||
} else {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you have ")
|
||||
}
|
||||
|
||||
g.addmsgf("defeated ")
|
||||
}
|
||||
|
||||
g.msg("%s", mname)
|
||||
}
|
||||
// Do adjustments if he went up a level
|
||||
g.checkLevel()
|
||||
|
||||
if g.Options.FightFlush {
|
||||
g.flushType()
|
||||
}
|
||||
}
|
||||
|
||||
// killedSpecial handles deaths with side effects: a flytrap releases its
|
||||
// grip and a leprechaun drops its gold (the switch of fight.c killed).
|
||||
func (g *RogueGame) killedSpecial(tp *Monster) {
|
||||
p := &g.Player
|
||||
|
||||
// If the monster was a venus flytrap, un-hold him
|
||||
switch tp.Type {
|
||||
case 'F':
|
||||
p.Flags.Clear(Held)
|
||||
p.VfHit = 0
|
||||
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
|
||||
case 'L':
|
||||
pos, ok := g.fallpos(tp.Pos)
|
||||
if ok {
|
||||
tp.Room.Gold = pos
|
||||
}
|
||||
|
||||
if ok && g.Depth >= g.MaxDepth {
|
||||
gold := newObject()
|
||||
gold.Kind = KindGold
|
||||
|
||||
gold.GoldValue = g.goldCalc()
|
||||
if g.save(VsMagic) {
|
||||
gold.GoldValue += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
|
||||
}
|
||||
|
||||
attachObj(&tp.Pack, gold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushType flushes typeahead for the fight_flush option (mach_dep.c
|
||||
// flush_type / curses flushinp).
|
||||
func (g *RogueGame) flushType() {
|
||||
if f, ok := g.scr.term.(interface{ FlushInput() }); ok {
|
||||
f.FlushInput()
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// mkGame builds a headless game on a generated level, initializing the
|
||||
// look() state the way playit() does before the first command.
|
||||
func mkGame(t *testing.T, seed int32) *RogueGame {
|
||||
t.Helper()
|
||||
|
||||
g := New(Params{Seed: seed, Term: &testTerm{}})
|
||||
g.NewLevel()
|
||||
g.Oldpos = g.Player.Pos
|
||||
g.Oldrp = g.roomIn(g.Player.Pos)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// spawnAdjacent puts a monster of the given type next to the hero.
|
||||
func spawnAdjacent(g *RogueGame, typ byte) *Monster {
|
||||
pos := Coord{X: g.Player.Pos.X + 1, Y: g.Player.Pos.Y}
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, typ, pos)
|
||||
|
||||
return tp
|
||||
}
|
||||
|
||||
func TestRollEmParsesMultiAttackDice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 42)
|
||||
att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: dice("1x4/1x4/1x4")}}
|
||||
def := &Creature{Stats: Stats{ArmorClass: 10, HP: 1000}}
|
||||
def.Flags.Set(Awake)
|
||||
// With attacker level 20 vs armor 10, swing always hits
|
||||
// (rnd(20)+wplus >= (20-20)-10 is always true), so three attacks of
|
||||
// 1x4 + str bonus 1 each must deal between 6 and 15 damage.
|
||||
if !g.rollAttacks(att, def, nil, false) {
|
||||
t.Fatal("attack with guaranteed swing missed")
|
||||
}
|
||||
|
||||
dmg := 1000 - def.Stats.HP
|
||||
if dmg < 6 || dmg > 15 {
|
||||
t.Errorf("three 1x4+1 attacks dealt %d damage, want 6..15", dmg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFightKillsMonster(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 7)
|
||||
tp := spawnAdjacent(g, 'B') // bat: 1 hit die
|
||||
tp.Stats.HP = 1
|
||||
g.Player.Stats.Lvl = 20 // always hits
|
||||
before := len(g.Level.Monsters)
|
||||
g.fight(tp.Pos, g.Player.CurWeapon, false)
|
||||
|
||||
if len(g.Level.Monsters) != before-1 {
|
||||
t.Error("monster not removed after fatal fight")
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(tp.Pos.Y, tp.Pos.X) != nil {
|
||||
t.Error("map still records dead monster")
|
||||
}
|
||||
|
||||
if g.Player.Stats.Exp == 0 {
|
||||
t.Error("no experience for the kill")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttackHurtsPlayer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 9)
|
||||
tp := spawnAdjacent(g, 'T') // troll: 1x8/1x8/2x6
|
||||
tp.Stats.Lvl = 20 // always hits
|
||||
tp.Flags.Clear(Cancelled)
|
||||
|
||||
hpBefore := g.Player.Stats.HP
|
||||
g.Player.Stats.HP = 500
|
||||
g.Player.Stats.MaxHP = 500
|
||||
g.attack(tp)
|
||||
|
||||
if g.Player.Stats.HP >= 500 {
|
||||
t.Errorf("player HP unchanged (%d -> %d)", hpBefore, g.Player.Stats.HP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnersChaseHero(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 3)
|
||||
// Place a hobgoblin a few squares away in the hero's room and set it
|
||||
// running at the hero.
|
||||
p := &g.Player
|
||||
|
||||
pos := Coord{X: p.Pos.X + 3, Y: p.Pos.Y}
|
||||
if !stepOk(g.Level.Char(pos.Y, pos.X)) || g.Level.MonsterAt(pos.Y, pos.X) != nil {
|
||||
t.Skip("no clear lane on this seed")
|
||||
}
|
||||
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, 'H', pos)
|
||||
tp.Flags.Set(Awake)
|
||||
tp.Dest = &p.Pos
|
||||
d0 := distCp(tp.Pos, p.Pos)
|
||||
|
||||
g.runners(0)
|
||||
|
||||
if d1 := distCp(tp.Pos, p.Pos); d1 >= d0 {
|
||||
t.Errorf("monster did not close distance: %d -> %d", d0, d1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKilledLeprechaunDropsGoldViaFall(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 21)
|
||||
tp := spawnAdjacent(g, 'L')
|
||||
tp.Stats.HP = 0
|
||||
objsBefore := len(g.Level.Objects)
|
||||
g.killed(tp, false)
|
||||
// At max depth the leprechaun's hoard falls to the floor.
|
||||
if len(g.Level.Objects) <= objsBefore-1 {
|
||||
t.Error("level object list shrank unexpectedly")
|
||||
}
|
||||
}
|
||||
357
game/game.go
357
game/game.go
@@ -1,357 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ItemLore is the per-game item identity state: the randomized appearance
|
||||
// names and the seven mutable ObjInfo tables (extern.c/init.c).
|
||||
type ItemLore struct {
|
||||
PotColors [NumPotionTypes]string // p_colors: colors of the potions
|
||||
ScrNames [NumScrollTypes]string // s_names: names of the scrolls
|
||||
RingStones [NumRingTypes]string // r_stones: stone settings of the rings
|
||||
WandMade [NumWandTypes]string // ws_made: what sticks are made of
|
||||
WandType [NumWandTypes]string // ws_type: "wand" or "staff"
|
||||
|
||||
Things [NumThings]ObjInfo
|
||||
Potions [NumPotionTypes]ObjInfo
|
||||
Scrolls [NumScrollTypes]ObjInfo
|
||||
Rings [NumRingTypes]ObjInfo
|
||||
Sticks [NumWandTypes]ObjInfo
|
||||
Weapons [NumWeaponTypes + 1]ObjInfo
|
||||
Armors [NumArmorTypes]ObjInfo
|
||||
|
||||
Group int // group number for the next stack of missiles (weapons.c `group`)
|
||||
}
|
||||
|
||||
// Options are the user-settable game options (options.c optlist).
|
||||
type Options struct {
|
||||
Terse bool // terse: shorter messages
|
||||
FightFlush bool // flush: flush typeahead during battle
|
||||
Jump bool // jump: show position only at end of run
|
||||
SeeFloor bool // seefloor: show the lamp-illuminated floor
|
||||
PassGo bool // passgo: follow turnings in passageways
|
||||
Tombstone bool // tombstone: print tombstone when killed
|
||||
InvType int // inven: inventory style (InvOver/InvSlow/InvClear)
|
||||
}
|
||||
|
||||
// Params carries everything needed to construct a game.
|
||||
type Params struct {
|
||||
Seed int32 // dungeon number; caller derives it (time+pid or SEED)
|
||||
Name string // player name (overridden by ROGUEOPTS name=)
|
||||
RogueOpts string // the ROGUEOPTS environment string
|
||||
Home string // home directory (save file default location)
|
||||
ScorePath string // scoreboard file; empty disables scoring
|
||||
Wizard bool // enable debug commands (implies NoScore)
|
||||
Term Terminal // the display/input device; nil runs headless
|
||||
}
|
||||
|
||||
// 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
|
||||
// played on. Construct with New, then call Run.
|
||||
//
|
||||
// The struct grows with the port; fields appear in the phase that ports the
|
||||
// code owning them.
|
||||
type RogueGame struct {
|
||||
// identity / RNG
|
||||
Rng *Rng
|
||||
Dnum int // dungeon number
|
||||
Whoami string // name of player
|
||||
Fruit string // favorite fruit
|
||||
Home string // user's home directory
|
||||
FileName string // save file name
|
||||
Wizard bool // true if allows wizard commands
|
||||
NoScore bool // was a wizard sometime
|
||||
|
||||
// the world
|
||||
Player Player
|
||||
Level Level
|
||||
Depth int // C `level`: what level she is on
|
||||
MaxDepth int // C `max_level`: deepest player has gone
|
||||
HasAmulet bool // C `amulet`: he found the amulet
|
||||
SeenStairs bool // have seen the stairs (for lsd)
|
||||
|
||||
// turn/command engine
|
||||
Playing bool // true until he quits
|
||||
After bool // true if we want after daemons
|
||||
Again bool // repeating the last command
|
||||
Count int // number of times to repeat command
|
||||
NoCommand int // number of turns asleep
|
||||
NoMove int // number of turns held in place
|
||||
Quiet int // number of quiet turns
|
||||
Running bool // true if player is running
|
||||
RunCh byte // direction player is running
|
||||
DoorStop bool // stop running when we pass a door
|
||||
Firstmove bool // first move after setting door_stop
|
||||
MoveOn bool // next move shouldn't pick up items
|
||||
ToDeath bool // fighting is to the death!
|
||||
Kamikaze bool // to_death really to DEATH
|
||||
HasHit bool // has a "hit" message pending in msg
|
||||
MaxHit int // max damage done to her in to_death
|
||||
Take byte // thing she is taking
|
||||
Delta Coord
|
||||
DirCh byte
|
||||
LastComm byte
|
||||
LastDir byte
|
||||
LLastComm byte
|
||||
LLastDir byte
|
||||
LastPick *Object
|
||||
LLastPick *Object
|
||||
lastDelt Coord // misc.c get_dir static last_delt
|
||||
// command.c statics
|
||||
countCh byte
|
||||
direction byte
|
||||
newCount bool
|
||||
|
||||
// dungeon generation working state
|
||||
maze mazeState // rooms.c maze statics
|
||||
pnum int // passages.c passnum statics
|
||||
newpnum bool
|
||||
chRet Coord // chase.c static ch_ret: where chasing takes you
|
||||
|
||||
// daemons/fuses
|
||||
Daemons DaemonList
|
||||
|
||||
// screen / messages
|
||||
scr *Screen
|
||||
Msgs MessageLine
|
||||
statusCache statusCache
|
||||
invPage invPage // things.c discovery-list pagination statics
|
||||
|
||||
// UI state
|
||||
StatMsg bool // should status() print as a msg()
|
||||
InvDescribe bool // say which way items are being used
|
||||
QComm bool // are we executing a 'Q' command?
|
||||
InShell bool // true if executing a shell
|
||||
Oldpos Coord // position before last look() call
|
||||
Oldrp *Room
|
||||
NObjs int // # items listed in inventory() call
|
||||
|
||||
// options and item identity
|
||||
Options Options
|
||||
Items ItemLore
|
||||
// Monsters is the per-game copy of the bestiary: the C code mutates
|
||||
// the venus flytrap's damage string during play, and the table is
|
||||
// part of the save state.
|
||||
Monsters [26]MonsterKind
|
||||
|
||||
// scores
|
||||
LastScore int
|
||||
AllScore bool
|
||||
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
|
||||
|
||||
// sigSave carries signal-triggered autosave requests from the signal
|
||||
// goroutine to the game goroutine, which is the only one allowed to
|
||||
// touch the state above (issue #24). Buffered by one: the handler
|
||||
// reads exactly one signal, so there is never more than one request.
|
||||
// See AutoSaveOnSignal and serviceAutoSaveRequest in save.go.
|
||||
sigSave chan *autoSaveRequest
|
||||
|
||||
// data is the game's copy of the static tables (extern.c and friends).
|
||||
data *gameData
|
||||
}
|
||||
|
||||
// Greeting is the line C printed on stdout while the player waited for
|
||||
// the dungeon to be dug, immediately before initscr() (main.c main). The
|
||||
// caller prints it before the terminal package takes the screen, which is
|
||||
// where initscr() sat; there is no trailing newline in either wording,
|
||||
// because C followed the printf with fflush and let curses have the
|
||||
// display.
|
||||
//
|
||||
// Only the wizard wording is #ifdef MASTER in C, and it carries the
|
||||
// dungeon number, which is the seed (main.c assigns seed = dnum right
|
||||
// after choosing dnum). The other wording is unconditional.
|
||||
//
|
||||
// The name is C's whoami, resolved the way main.c resolves it: parse_opts
|
||||
// runs before the printf, so a ROGUEOPTS "name=" setting is what the
|
||||
// player is greeted by, and the account name is only the fallback. New
|
||||
// does the same parse a moment later; doing it here too is safe because
|
||||
// ParseOpts does nothing but assign into the fields it is handed — no
|
||||
// RNG, no screen — so it cannot disturb the item tables the seed-compat
|
||||
// golden pins.
|
||||
//
|
||||
// The game it parses into is a throwaway, but it is built the way New
|
||||
// builds the real one, because ParseOpts handles every option and not
|
||||
// just the one this function reads: "inven=" is matched against the
|
||||
// inv_t_name[] table and "file=~/..." against the home directory, both
|
||||
// of which live on the game. A greeting that skimped on them faulted on
|
||||
// a perfectly legal ROGUEOPTS before the player saw a single character.
|
||||
func Greeting(params Params) string {
|
||||
whoami := params.Name
|
||||
|
||||
if params.RogueOpts != "" {
|
||||
opts := &RogueGame{
|
||||
data: newGameData(),
|
||||
Whoami: params.Name,
|
||||
Home: params.Home,
|
||||
}
|
||||
opts.ParseOpts(params.RogueOpts)
|
||||
|
||||
whoami = opts.Whoami
|
||||
}
|
||||
|
||||
if params.Wizard {
|
||||
return fmt.Sprintf("Hello %s, welcome to dungeon #%d",
|
||||
whoami, params.Seed)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"Hello %s, just a moment while I dig the dungeon...", whoami)
|
||||
}
|
||||
|
||||
// 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
|
||||
// and first level arrive with later porting phases).
|
||||
func New(params Params) *RogueGame {
|
||||
g := &RogueGame{
|
||||
data: newGameData(),
|
||||
Rng: &Rng{Seed: params.Seed},
|
||||
Dnum: int(params.Seed),
|
||||
Whoami: params.Name,
|
||||
Fruit: "slime-mold",
|
||||
Home: params.Home,
|
||||
Wizard: params.Wizard,
|
||||
NoScore: params.Wizard,
|
||||
Playing: true,
|
||||
Depth: 1,
|
||||
ScorePath: params.ScorePath,
|
||||
LastScore: -1,
|
||||
sigSave: make(chan *autoSaveRequest, 1),
|
||||
}
|
||||
g.Options = Options{
|
||||
SeeFloor: true,
|
||||
Tombstone: true,
|
||||
InvType: InvOver,
|
||||
}
|
||||
g.InvDescribe = true
|
||||
g.Msgs.SaveMsg = true
|
||||
g.scr = NewScreen(params.Term)
|
||||
g.Msgs.attach(g.scr, g.look, g.readchar)
|
||||
g.FileName = params.Home + "/rogue.save"
|
||||
|
||||
g.rogueOpts = params.RogueOpts
|
||||
if params.Wizard {
|
||||
g.Player.Flags.Set(SenseMonsters)
|
||||
}
|
||||
|
||||
if params.RogueOpts != "" {
|
||||
g.ParseOpts(params.RogueOpts)
|
||||
}
|
||||
|
||||
g.Monsters = g.data.monsterTable
|
||||
|
||||
g.Items.Group = 2 // weapons.c: int group = 2
|
||||
for i := range g.Level.Passages {
|
||||
g.Level.Passages[i].Flags = Gone | Dark
|
||||
}
|
||||
|
||||
g.initProbs() // set up prob tables for objects
|
||||
g.initPlayer() // set up initial player stats
|
||||
g.initNames() // set up names of scrolls
|
||||
g.initColors() // set up colors of potions
|
||||
g.initStones() // set up stone settings of rings
|
||||
g.initMaterials() // set up materials of wands
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// Run plays the game to its end: the back half of main.c main() plus
|
||||
// playit(). It does not return — the game ends by exiting the process
|
||||
// (see myExit); one game run is one process.
|
||||
func (g *RogueGame) Run() {
|
||||
g.startLevel()
|
||||
g.playit()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// playit is the main loop of the program (main.c 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
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// quit has the player make certain, then exits (main.c quit). The final
|
||||
// scoring display arrives with the endgame phase.
|
||||
func (g *RogueGame) quit(int) {
|
||||
// Reset the message position in case we got here via an interrupt
|
||||
if !g.QComm {
|
||||
g.Msgs.Mpos = 0
|
||||
}
|
||||
|
||||
oy, ox := g.scr.Std.GetYX()
|
||||
g.msg("really quit?")
|
||||
|
||||
if g.readchar() == 'y' {
|
||||
g.clear()
|
||||
g.scr.Std.MvPrintwf(NumLines-2, 0, "You quit with %d gold pieces", g.Player.Purse)
|
||||
g.move(NumLines-1, 0)
|
||||
g.refresh()
|
||||
g.score(g.Player.Purse, 1, 0)
|
||||
g.myExit()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.move(0, 0)
|
||||
g.clrtoeol()
|
||||
g.status()
|
||||
g.move(oy, ox)
|
||||
g.refresh()
|
||||
g.Msgs.Mpos = 0
|
||||
g.Count = 0
|
||||
g.ToDeath = false
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestGreeting pins both wordings of main.c's pre-initscr printf byte for
|
||||
// byte. The wizard one carries dnum, which main.c has just assigned to
|
||||
// seed, so it is the seed the player sees. Neither ends in a newline: C
|
||||
// printed, flushed, and handed the display to curses.
|
||||
func TestGreeting(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// The account name main.c copies into whoami when ROGUEOPTS does not
|
||||
// name the player itself.
|
||||
const account = "conan"
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
params Params
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
params: Params{Name: account, Seed: 4242},
|
||||
want: "Hello conan, just a moment while I dig the dungeon...",
|
||||
},
|
||||
{
|
||||
name: "wizard names the dungeon",
|
||||
params: Params{Name: account, Seed: 4242, Wizard: true},
|
||||
want: "Hello conan, welcome to dungeon #4242",
|
||||
},
|
||||
{
|
||||
// parse_opts runs before the printf in main.c, and whoami
|
||||
// falls back to the account name only when ROGUEOPTS left it
|
||||
// empty, so the option is what the player is greeted by.
|
||||
name: "ROGUEOPTS name wins over the account name",
|
||||
params: Params{
|
||||
Name: account, Seed: 7, RogueOpts: "name=Rodney",
|
||||
},
|
||||
want: "Hello Rodney, just a moment while I dig the dungeon...",
|
||||
},
|
||||
{
|
||||
name: "ROGUEOPTS without a name keeps the account name",
|
||||
params: Params{
|
||||
Name: account, Seed: 7, RogueOpts: "terse,fruit=mango",
|
||||
},
|
||||
want: "Hello conan, just a moment while I dig the dungeon...",
|
||||
},
|
||||
{
|
||||
// ParseOpts reaches every option, not just name=, and the
|
||||
// inventory style is matched against a table (options.c
|
||||
// parse_opts, inv_t_name[]) that lives in the game data. A
|
||||
// greeting parsed on a game without those tables faulted on
|
||||
// this ROGUEOPTS before it could print anything at all.
|
||||
name: "ROGUEOPTS inventory style parses without a fault",
|
||||
params: Params{
|
||||
Name: account, Seed: 7, RogueOpts: "inven=slow,name=Rodney",
|
||||
},
|
||||
want: "Hello Rodney, just a moment while I dig the dungeon...",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := Greeting(tc.params)
|
||||
if got != tc.want {
|
||||
t.Errorf("Greeting() = %q, want %q", got, tc.want)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(got, "\n") {
|
||||
t.Error("greeting ends in a newline; C's printf did not")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
186
game/init.go
186
game/init.go
@@ -1,186 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "strings"
|
||||
|
||||
// init.c — per-game randomization of item appearances and probability
|
||||
// tables, and the player roll-up.
|
||||
|
||||
// initPlayer rolls her up (init.c init_player).
|
||||
func (g *RogueGame) initPlayer() {
|
||||
p := &g.Player
|
||||
p.MaxStats = g.data.initStats
|
||||
p.Stats = p.MaxStats
|
||||
p.FoodLeft = HungerTime
|
||||
// Give him some food
|
||||
obj := newObject()
|
||||
obj.Kind = KindFood
|
||||
obj.Count = 1
|
||||
g.addPack(obj, true)
|
||||
// And his suit of armor
|
||||
obj = newObject()
|
||||
obj.Kind = KindArmor
|
||||
obj.Which = int(ArmorRingMail)
|
||||
obj.ArmorClass = g.data.aClass[ArmorRingMail] - 1
|
||||
obj.Flags.Set(Known)
|
||||
obj.Count = 1
|
||||
p.CurArmor = obj
|
||||
g.addPack(obj, true)
|
||||
// Give him his weaponry. First a mace.
|
||||
obj = newObject()
|
||||
g.initWeapon(obj, WeaponMace)
|
||||
obj.HPlus = 1
|
||||
obj.DPlus = 1
|
||||
obj.Flags.Set(Known)
|
||||
g.addPack(obj, true)
|
||||
p.CurWeapon = obj
|
||||
// Now a +1 bow
|
||||
obj = newObject()
|
||||
g.initWeapon(obj, WeaponBow)
|
||||
obj.HPlus = 1
|
||||
obj.Flags.Set(Known)
|
||||
g.addPack(obj, true)
|
||||
// Now some arrows
|
||||
obj = newObject()
|
||||
g.initWeapon(obj, WeaponArrow)
|
||||
obj.Count = g.rnd(15) + 25
|
||||
obj.Flags.Set(Known)
|
||||
g.addPack(obj, true)
|
||||
}
|
||||
|
||||
// initColors initializes the potion color scheme for this game
|
||||
// (init.c init_colors).
|
||||
func (g *RogueGame) initColors() {
|
||||
used := make([]bool, len(g.data.rainbow))
|
||||
|
||||
for i := range NumPotionTypes {
|
||||
var j int
|
||||
for {
|
||||
j = g.rnd(len(g.data.rainbow))
|
||||
if !used[j] {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
used[j] = true
|
||||
g.Items.PotColors[i] = g.data.rainbow[j]
|
||||
}
|
||||
}
|
||||
|
||||
// initNames generates the names of the various scrolls (init.c init_names).
|
||||
func (g *RogueGame) initNames() {
|
||||
for i := range NumScrollTypes {
|
||||
var cp strings.Builder
|
||||
|
||||
nwords := g.rnd(3) + 2
|
||||
for ; nwords > 0; nwords-- {
|
||||
nsyl := g.rnd(3) + 1
|
||||
for ; nsyl > 0; nsyl-- {
|
||||
sp := g.data.sylls[g.rnd(len(g.data.sylls))]
|
||||
if cp.Len()+len(sp) > MaxNameLen {
|
||||
break
|
||||
}
|
||||
|
||||
cp.WriteString(sp)
|
||||
}
|
||||
|
||||
cp.WriteByte(' ')
|
||||
}
|
||||
|
||||
g.Items.ScrNames[i] = strings.TrimSuffix(cp.String(), " ")
|
||||
}
|
||||
}
|
||||
|
||||
// initStones initializes the ring stone setting scheme for this game
|
||||
// (init.c init_stones).
|
||||
func (g *RogueGame) initStones() {
|
||||
used := make([]bool, len(g.data.stoneTable))
|
||||
|
||||
for i := range NumRingTypes {
|
||||
var j int
|
||||
for {
|
||||
j = g.rnd(len(g.data.stoneTable))
|
||||
if !used[j] {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
used[j] = true
|
||||
g.Items.RingStones[i] = g.data.stoneTable[j].Name
|
||||
g.Items.Rings[i].Worth += g.data.stoneTable[j].Value
|
||||
}
|
||||
}
|
||||
|
||||
// initMaterials initializes the construction materials for wands and staffs
|
||||
// (init.c init_materials).
|
||||
func (g *RogueGame) initMaterials() {
|
||||
used := make([]bool, len(g.data.woods))
|
||||
metused := make([]bool, len(g.data.metals))
|
||||
|
||||
for i := range NumWandTypes {
|
||||
var str string
|
||||
|
||||
for {
|
||||
if g.rnd(2) == 0 {
|
||||
j := g.rnd(len(g.data.metals))
|
||||
if !metused[j] {
|
||||
g.Items.WandType[i] = wandName
|
||||
str = g.data.metals[j]
|
||||
metused[j] = true
|
||||
|
||||
break
|
||||
}
|
||||
} else {
|
||||
j := g.rnd(len(g.data.woods))
|
||||
if !used[j] {
|
||||
g.Items.WandType[i] = staffName
|
||||
str = g.data.woods[j]
|
||||
used[j] = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.Items.WandMade[i] = str
|
||||
}
|
||||
}
|
||||
|
||||
// sumProbs sums up the probabilities for items appearing, converting the
|
||||
// per-item weights into a cumulative distribution (init.c sumprobs).
|
||||
func sumProbs(info []ObjInfo) {
|
||||
for i := 1; i < len(info); i++ {
|
||||
info[i].Prob += info[i-1].Prob
|
||||
}
|
||||
}
|
||||
|
||||
// initProbs copies the base tables into the game and initializes the
|
||||
// probabilities for the various items (init.c init_probs).
|
||||
func (g *RogueGame) initProbs() {
|
||||
g.Items.Things = g.data.baseThings
|
||||
g.Items.Potions = g.data.basePotInfo
|
||||
g.Items.Scrolls = g.data.baseScrInfo
|
||||
g.Items.Rings = g.data.baseRingInfo
|
||||
g.Items.Sticks = g.data.baseWsInfo
|
||||
g.Items.Weapons = g.data.baseWeapInfo
|
||||
g.Items.Armors = g.data.baseArmInfo
|
||||
|
||||
sumProbs(g.Items.Things[:])
|
||||
sumProbs(g.Items.Potions[:])
|
||||
sumProbs(g.Items.Scrolls[:])
|
||||
sumProbs(g.Items.Rings[:])
|
||||
sumProbs(g.Items.Sticks[:])
|
||||
// C sums MAXWEAPONS, excluding the flame entry.
|
||||
sumProbs(g.Items.Weapons[:NumWeaponTypes])
|
||||
sumProbs(g.Items.Armors[:])
|
||||
}
|
||||
|
||||
// pickColor returns the given color, or a random one if the hero is
|
||||
// hallucinating (init.c pick_color).
|
||||
func (g *RogueGame) pickColor(col string) string {
|
||||
if g.Player.On(Hallucinating) {
|
||||
return g.data.rainbow[g.rnd(len(g.data.rainbow))]
|
||||
}
|
||||
|
||||
return col
|
||||
}
|
||||
332
game/io.go
332
game/io.go
@@ -1,332 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// io.c — the message line, the status line, and character input.
|
||||
|
||||
// maxMsg is io.c MAXMSG: how much message fits before --More--.
|
||||
const maxMsg = NumCols - len("--More--") - 1
|
||||
|
||||
// MessageLine is the io.c message machinery: the static msgbuf/newpos
|
||||
// pair plus the related globals (mpos, huh, and the message-behavior
|
||||
// flags). It owns the top line of the screen; attach wires in the
|
||||
// display and input it needs.
|
||||
type MessageLine struct {
|
||||
buf strings.Builder // msgbuf
|
||||
newpos int
|
||||
Mpos int // where cursor is on top line
|
||||
Huh string // the last message printed
|
||||
SaveMsg bool // remember last msg
|
||||
LowerMsg bool // messages should start w/lower case
|
||||
MsgEsc bool // check for ESC from msg's --More--
|
||||
|
||||
scr *Screen // the top line lives on scr.Std
|
||||
look func(wakeup bool) // redraw before a --More-- (misc.c look)
|
||||
readChar func() byte // input for --More-- prompts
|
||||
}
|
||||
|
||||
// Msg displays a message at the top of the screen (io.c msg). It returns
|
||||
// Escape if the player escaped out of a --More--, ^Escape otherwise (the
|
||||
// C convention: callers compare against ESCAPE).
|
||||
func (m *MessageLine) Msg(format string, a ...any) int {
|
||||
// if the string is "", just clear the line
|
||||
if format == "" {
|
||||
m.scr.Std.Move(0, 0)
|
||||
m.scr.Std.Clrtoeol()
|
||||
m.Mpos = 0
|
||||
|
||||
return ^Escape
|
||||
}
|
||||
// otherwise add to the message and flush it out
|
||||
m.doaddf(format, a...)
|
||||
|
||||
return m.End()
|
||||
}
|
||||
|
||||
// Addf adds things to the current message (io.c addmsg).
|
||||
func (m *MessageLine) Addf(format string, a ...any) {
|
||||
m.doaddf(format, a...)
|
||||
}
|
||||
|
||||
// End displays a new msg, giving the player a chance to see the previous
|
||||
// one if it is up there with the --More-- (io.c endmsg).
|
||||
func (m *MessageLine) End() int {
|
||||
if m.SaveMsg {
|
||||
m.Huh = m.buf.String()
|
||||
}
|
||||
|
||||
if m.Mpos != 0 && m.promptMore() == Escape {
|
||||
return Escape
|
||||
}
|
||||
// All messages should start with uppercase, except ones that start
|
||||
// with a pack addressing character
|
||||
out := m.buf.String()
|
||||
if len(out) > 0 && isLower(out[0]) && !m.LowerMsg &&
|
||||
(len(out) <= 1 || out[1] != ')') {
|
||||
out = string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
|
||||
m.scr.Std.MvAddStr(0, 0, out)
|
||||
m.scr.Std.Clrtoeol()
|
||||
|
||||
m.Mpos = m.newpos
|
||||
m.newpos = 0
|
||||
m.buf.Reset()
|
||||
m.scr.Refresh()
|
||||
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
// promptMore shows the --More-- prompt and waits for the reader to
|
||||
// acknowledge; Escape means the player bailed out (the Mpos block of
|
||||
// io.c endmsg).
|
||||
func (m *MessageLine) promptMore() int {
|
||||
m.look(false)
|
||||
m.scr.Std.MvAddStr(0, m.Mpos, "--More--")
|
||||
m.scr.Refresh()
|
||||
|
||||
if !m.MsgEsc {
|
||||
m.waitForSpace()
|
||||
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
for {
|
||||
ch := m.readChar()
|
||||
if ch == ' ' {
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
if ch == Escape {
|
||||
m.buf.Reset()
|
||||
m.Mpos = 0
|
||||
m.newpos = 0
|
||||
|
||||
return Escape
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attach wires the message line to its display and input; NewGame and
|
||||
// Restore call it once the screen and game exist.
|
||||
func (m *MessageLine) attach(scr *Screen, look func(bool), readChar func() byte) {
|
||||
m.scr = scr
|
||||
m.look = look
|
||||
m.readChar = readChar
|
||||
}
|
||||
|
||||
// waitForSpace absorbs input until the player types a space: the
|
||||
// --More-- acknowledgement (io.c wait_for).
|
||||
func (m *MessageLine) waitForSpace() {
|
||||
for {
|
||||
if m.readChar() == ' ' {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doaddf performs an add onto the message buffer (io.c doadd).
|
||||
func (m *MessageLine) doaddf(format string, a ...any) {
|
||||
s := fmt.Sprintf(format, a...)
|
||||
if len(s)+m.newpos >= maxMsg {
|
||||
m.End()
|
||||
}
|
||||
|
||||
m.buf.WriteString(s)
|
||||
m.newpos = m.buf.Len()
|
||||
}
|
||||
|
||||
// msg, addmsgf, and endmsg are the game-side shorthands for the message
|
||||
// line; the machinery lives on MessageLine.
|
||||
func (g *RogueGame) msg(format string, a ...any) int {
|
||||
return g.Msgs.Msg(format, a...)
|
||||
}
|
||||
|
||||
func (g *RogueGame) addmsgf(format string, a ...any) {
|
||||
g.Msgs.Addf(format, a...)
|
||||
}
|
||||
|
||||
func (g *RogueGame) endmsg() {
|
||||
g.Msgs.End()
|
||||
}
|
||||
|
||||
// stepOk returns true if it is ok to step on ch (io.c step_ok).
|
||||
func stepOk(ch byte) bool {
|
||||
switch ch {
|
||||
case ' ', '|', '-':
|
||||
return false
|
||||
default:
|
||||
return !isAlpha(ch)
|
||||
}
|
||||
}
|
||||
|
||||
// readchar reads and returns a character, checking for gross input errors
|
||||
// (io.c readchar).
|
||||
//
|
||||
// Waiting for a key is where the game spends nearly all of its wall
|
||||
// clock, so it is also where a signal-triggered autosave usually finds
|
||||
// it: a dropped connection lands while the player is thinking, not
|
||||
// mid-turn. Terminal.Interrupt wakes the read for exactly that, and the
|
||||
// save runs here, on the game goroutine, before reading again.
|
||||
//
|
||||
// What that buys is a snapshot taken by the goroutine that owns the
|
||||
// state, so it is internally consistent and restorable. It is never a
|
||||
// between-commands snapshot: readchar is reached from readCommand at the
|
||||
// top of a turn that has already run its BEFORE daemons and turnUpkeep,
|
||||
// and from prompts raised part-way through a command — --More--,
|
||||
// askOverwrite, getStr, the direction and pack prompts — by which point
|
||||
// the command has mutated state as well. See serviceAutoSaveRequest
|
||||
// (save.go) for the full statement of what the handoff guarantees and
|
||||
// what it costs the player.
|
||||
func (g *RogueGame) readchar() byte {
|
||||
for {
|
||||
ch, ok := g.scr.term.ReadChar()
|
||||
if !ok {
|
||||
g.serviceAutoSaveRequest()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == 3 { // ^C
|
||||
g.quit(0)
|
||||
|
||||
return 27
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
}
|
||||
|
||||
// statusCache is the set of static shadow variables in io.c status() that
|
||||
// suppress redundant status-line redraws.
|
||||
type statusCache struct {
|
||||
hpwidth int
|
||||
hungry int
|
||||
lvl int
|
||||
pur int
|
||||
hp int
|
||||
arm int
|
||||
str int
|
||||
exp int
|
||||
init bool
|
||||
}
|
||||
|
||||
// status displays the important stats line, keeping the cursor where it was
|
||||
// (io.c status).
|
||||
func (g *RogueGame) status() {
|
||||
s := &g.statusCache
|
||||
p := &g.Player
|
||||
|
||||
// If nothing has changed since the last status, don't bother.
|
||||
temp := p.Stats.ArmorClass
|
||||
if p.CurArmor != nil {
|
||||
temp = p.CurArmor.ArmorClass
|
||||
}
|
||||
|
||||
if g.statusUnchanged(temp) {
|
||||
return
|
||||
}
|
||||
|
||||
s.init = true
|
||||
s.arm = temp
|
||||
|
||||
oy, ox := g.scr.Std.GetYX()
|
||||
|
||||
if s.hp != p.Stats.MaxHP {
|
||||
s.hp = p.Stats.MaxHP
|
||||
|
||||
s.hpwidth = 0
|
||||
for t := p.Stats.MaxHP; t != 0; t /= 10 {
|
||||
s.hpwidth++
|
||||
}
|
||||
}
|
||||
|
||||
// Save current status
|
||||
s.lvl = g.Depth
|
||||
s.pur = p.Purse
|
||||
s.hp = p.Stats.HP
|
||||
s.str = p.Stats.Str
|
||||
s.exp = p.Stats.Exp
|
||||
s.hungry = p.HungryState
|
||||
|
||||
line := fmt.Sprintf(
|
||||
"Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s",
|
||||
g.Depth, p.Purse, s.hpwidth, p.Stats.HP, s.hpwidth, p.Stats.MaxHP,
|
||||
p.Stats.Str, p.MaxStats.Str, 10-s.arm, p.Stats.Lvl, p.Stats.Exp,
|
||||
g.data.hungerStateName[p.HungryState])
|
||||
if g.StatMsg {
|
||||
g.move(0, 0)
|
||||
g.msg("%s", line)
|
||||
} else {
|
||||
g.move(StatLine, 0)
|
||||
g.addstr(line)
|
||||
}
|
||||
|
||||
g.clrtoeol()
|
||||
g.move(oy, ox)
|
||||
}
|
||||
|
||||
// statusUnchanged reports whether the status line still shows current
|
||||
// values, so it need not be redrawn (the shadow-variable check of io.c
|
||||
// status). temp is the effective armor class.
|
||||
func (g *RogueGame) statusUnchanged(temp int) bool {
|
||||
s := &g.statusCache
|
||||
p := &g.Player
|
||||
|
||||
return s.init && s.hp == p.Stats.HP && s.exp == p.Stats.Exp &&
|
||||
s.pur == p.Purse && s.arm == temp && s.str == p.Stats.Str &&
|
||||
s.lvl == g.Depth && s.hungry == p.HungryState && !g.StatMsg
|
||||
}
|
||||
|
||||
// waitFor sits around until the guy types the right key (io.c wait_for).
|
||||
func (g *RogueGame) waitFor(ch byte) {
|
||||
if ch == '\n' {
|
||||
for {
|
||||
c := g.readchar()
|
||||
if c == '\n' || c == '\r' {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if g.readchar() == ch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// showWin displays a window and waits before returning (io.c show_win).
|
||||
func (g *RogueGame) showWin(message string) {
|
||||
g.scr.Hw.MvAddStr(0, 0, message)
|
||||
g.scr.Hw.Move(g.Player.Pos.Y, g.Player.Pos.X)
|
||||
g.scr.RefreshWin(g.scr.Hw)
|
||||
g.waitFor(' ')
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
// ASCII helpers standing in for <ctype.h>; the C game only ever handles
|
||||
// 7-bit characters.
|
||||
func isAlpha(c byte) bool { return isUpper(c) || isLower(c) }
|
||||
func isUpper(c byte) bool { return c >= 'A' && c <= 'Z' }
|
||||
func isLower(c byte) bool { return c >= 'a' && c <= 'z' }
|
||||
func isDigit(c byte) bool { return c >= '0' && c <= '9' }
|
||||
func isPrint(c byte) bool { return c >= ' ' && c < 0x7f }
|
||||
func toUpper(c byte) byte {
|
||||
if isLower(c) {
|
||||
return c - 'a' + 'A'
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
func toLower(c byte) byte {
|
||||
if isUpper(c) {
|
||||
return c - 'A' + 'a'
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// Place describes a spot on the level map (rogue.h PLACE).
|
||||
type Place struct {
|
||||
Ch byte // the base map character
|
||||
Flags PlaceFlags
|
||||
Monst *Monster
|
||||
}
|
||||
|
||||
// Level is the current dungeon level: the map and everything on it. It is
|
||||
// reset in place by NewLevel, matching the C reuse of the global arrays.
|
||||
type Level struct {
|
||||
Places [MaxLines * MaxCols]Place
|
||||
Rooms [MaxRooms]Room
|
||||
Passages [MaxPass]Room // one pseudo-room per passage network
|
||||
Objects []*Object // lvl_obj: objects on this level
|
||||
Monsters []*Monster // mlist: monsters on the level
|
||||
Stairs Coord // location of the staircase
|
||||
TrapCount int // number of traps on this level
|
||||
}
|
||||
|
||||
// At returns the map cell at (y, x); the C INDEX(y,x) macro, including its
|
||||
// column-major (x<<5)+y layout.
|
||||
func (l *Level) At(y, x int) *Place {
|
||||
return &l.Places[(x<<5)+y]
|
||||
}
|
||||
|
||||
// Char is the chat(y,x) macro: the base map character at a spot.
|
||||
func (l *Level) Char(y, x int) byte { return l.At(y, x).Ch }
|
||||
|
||||
// SetChar updates the base map character at a spot.
|
||||
func (l *Level) SetChar(y, x int, ch byte) { l.At(y, x).Ch = ch }
|
||||
|
||||
// FlagsAt is the flat(y,x) macro, returned as a pointer so ported
|
||||
// read-modify-write sites keep their shape.
|
||||
func (l *Level) FlagsAt(y, x int) *PlaceFlags { return &l.At(y, x).Flags }
|
||||
|
||||
// MonsterAt is the moat(y,x) macro: the monster standing at a spot, if any.
|
||||
func (l *Level) MonsterAt(y, x int) *Monster { return l.At(y, x).Monst }
|
||||
|
||||
// SetMonsterAt places (or clears, with nil) the monster at a spot.
|
||||
func (l *Level) SetMonsterAt(y, x int, m *Monster) { l.At(y, x).Monst = m }
|
||||
|
||||
// VisibleChar is the winat(y,x) macro: what is apparently at a spot — a
|
||||
// monster's disguise if one stands there, else the map character.
|
||||
func (l *Level) VisibleChar(y, x int) byte {
|
||||
if m := l.MonsterAt(y, x); m != nil {
|
||||
return m.Disguise
|
||||
}
|
||||
|
||||
return l.Char(y, x)
|
||||
}
|
||||
|
||||
// ObjectAt finds the unclaimed object at (y, x) (misc.c find_obj).
|
||||
func (l *Level) ObjectAt(y, x int) *Object {
|
||||
for _, obj := range l.Objects {
|
||||
if obj.Pos.Y == y && obj.Pos.X == x {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddObject puts an object on the level (list.c attach on lvl_obj).
|
||||
func (l *Level) AddObject(obj *Object) { attachObj(&l.Objects, obj) }
|
||||
|
||||
// RemoveObject takes an object off the level (list.c detach on lvl_obj).
|
||||
func (l *Level) RemoveObject(obj *Object) { detachObj(&l.Objects, obj) }
|
||||
|
||||
// AddMonster puts a monster on the level (list.c attach on mlist).
|
||||
func (l *Level) AddMonster(m *Monster) { attachMon(&l.Monsters, m) }
|
||||
|
||||
// RemoveMonster takes a monster off the level (list.c detach on mlist).
|
||||
func (l *Level) RemoveMonster(m *Monster) { detachMon(&l.Monsters, m) }
|
||||
|
||||
// goldCalc is the GOLDCALC macro: how much a gold pile is worth at depth.
|
||||
func (g *RogueGame) goldCalc() int {
|
||||
return g.rnd(50+10*g.Depth) + 2
|
||||
}
|
||||
613
game/misc.go
613
game/misc.go
@@ -1,613 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// misc.c — look() display maintenance, direction input, eating, level-ups,
|
||||
// and small utilities. call_it arrives with the scroll/potion phase (it
|
||||
// needs the get_str line editor).
|
||||
|
||||
// lookScan carries the state of one look() glance while it examines the
|
||||
// nine squares around the hero.
|
||||
type lookScan struct {
|
||||
hero Coord
|
||||
pch byte // map character under the hero
|
||||
pfl PlaceFlags // map flags under the hero
|
||||
wakeup bool
|
||||
doorStop bool // door-stop checking applies (mid-run)
|
||||
sy, sx, ey, ex int
|
||||
sumhero, diffhero int
|
||||
passcount int
|
||||
}
|
||||
|
||||
// look takes a quick glance all around the player (misc.c look).
|
||||
func (g *RogueGame) look(wakeup bool) {
|
||||
p := &g.Player
|
||||
hero := p.Pos
|
||||
rp := p.Room
|
||||
|
||||
if g.Oldpos != hero {
|
||||
g.eraseLamp(g.Oldpos, g.Oldrp)
|
||||
g.Oldpos = hero
|
||||
g.Oldrp = rp
|
||||
}
|
||||
|
||||
s := lookScan{
|
||||
hero: hero,
|
||||
wakeup: wakeup,
|
||||
sy: hero.Y - 1,
|
||||
sx: hero.X - 1,
|
||||
ey: hero.Y + 1,
|
||||
ex: hero.X + 1,
|
||||
}
|
||||
|
||||
s.doorStop = g.DoorStop && !g.Firstmove
|
||||
if s.doorStop && g.Running {
|
||||
s.sumhero = hero.Y + hero.X
|
||||
s.diffhero = hero.Y - hero.X
|
||||
}
|
||||
|
||||
pp := g.Level.At(hero.Y, hero.X)
|
||||
s.pch = pp.Ch
|
||||
s.pfl = pp.Flags
|
||||
|
||||
g.lookAround(&s)
|
||||
|
||||
if s.doorStop && s.passcount > 1 {
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
if !g.Running || !g.Options.Jump {
|
||||
g.mvaddch(hero.Y, hero.X, PlayerCh)
|
||||
}
|
||||
}
|
||||
|
||||
// lookAround runs the nine-square scan of look().
|
||||
func (g *RogueGame) lookAround(s *lookScan) {
|
||||
for y := s.sy; y <= s.ey; y++ {
|
||||
if y <= 0 || y >= NumLines-1 {
|
||||
continue
|
||||
}
|
||||
|
||||
for x := s.sx; x <= s.ex; x++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
|
||||
g.lookCell(s, y, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lookCell examines one square around the hero: visibility rules, trip
|
||||
// and monster rendering, drawing, and run-stop checks (the loop body of
|
||||
// misc.c look).
|
||||
func (g *RogueGame) lookCell(s *lookScan, y, x int) {
|
||||
pp := g.Level.At(y, x)
|
||||
if g.lookSkips(s, pp, y, x) {
|
||||
return
|
||||
}
|
||||
|
||||
tp := pp.Monst
|
||||
|
||||
ch, skip := g.lookCellChar(s, tp, y, x, pp.Ch)
|
||||
if skip {
|
||||
return
|
||||
}
|
||||
|
||||
if !g.lookShow(s, tp, ch, y, x) {
|
||||
return
|
||||
}
|
||||
|
||||
if s.doorStop && g.Running {
|
||||
g.lookRunCheck(s, ch, y, x)
|
||||
}
|
||||
}
|
||||
|
||||
// lookSkips reports whether look ignores this square entirely: the
|
||||
// hero's own square when sighted, blank rock, passage squares of
|
||||
// another network, and diagonals the hero could not step to (the guard
|
||||
// chain of the misc.c look loop).
|
||||
func (g *RogueGame) lookSkips(s *lookScan, pp *Place, y, x int) bool {
|
||||
if !g.Player.On(Blind) && y == s.hero.Y && x == s.hero.X {
|
||||
return true
|
||||
}
|
||||
|
||||
if pp.Ch == ' ' { // nothing need be done with a ' '
|
||||
return true
|
||||
}
|
||||
|
||||
return lookForeignPassage(s, pp.Flags, pp.Ch) ||
|
||||
g.lookDiagonalBlocked(s, pp.Flags, pp.Ch, y, x)
|
||||
}
|
||||
|
||||
// lookForeignPassage hides passage squares belonging to a different
|
||||
// passage network than the hero's (misc.c look).
|
||||
func lookForeignPassage(s *lookScan, fp PlaceFlags, ch byte) bool {
|
||||
if s.pch != Door && ch != Door {
|
||||
return (s.pfl & FPassage) != (fp & FPassage)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// lookDiagonalBlocked hides diagonal door/passage squares the hero could
|
||||
// not actually step to (misc.c look).
|
||||
func (g *RogueGame) lookDiagonalBlocked(
|
||||
s *lookScan, fp PlaceFlags, ch byte, y, x int,
|
||||
) bool {
|
||||
if !fp.Has(FPassage) && ch != Door {
|
||||
return false
|
||||
}
|
||||
|
||||
if !s.pfl.Has(FPassage) && s.pch != Door {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.hero.X != x && s.hero.Y != y &&
|
||||
!stepOk(g.Level.Char(y, s.hero.X)) && !stepOk(g.Level.Char(s.hero.Y, x))
|
||||
}
|
||||
|
||||
// lookShow draws the square if it changed; it reports false when a
|
||||
// blind hero cannot see it at all (the draw part of the look loop).
|
||||
func (g *RogueGame) lookShow(s *lookScan, tp *Monster, ch byte, y, x int) bool {
|
||||
p := &g.Player
|
||||
if p.On(Blind) && (y != s.hero.Y || x != s.hero.X) {
|
||||
return false
|
||||
}
|
||||
|
||||
g.move(y, x)
|
||||
|
||||
if p.Room.Flags.Has(Dark) && !g.Options.SeeFloor && ch == Floor {
|
||||
ch = ' '
|
||||
}
|
||||
|
||||
if tp != nil || ch != g.inch() {
|
||||
g.addch(ch)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// lookCellChar picks what the square shows: trip rendering for empty
|
||||
// squares, waking and disguises for monsters. skip means the square is
|
||||
// not drawn at all (the monster switch of the look loop).
|
||||
func (g *RogueGame) lookCellChar(
|
||||
s *lookScan, tp *Monster, y, x int, ch byte,
|
||||
) (byte, bool) {
|
||||
p := &g.Player
|
||||
|
||||
switch {
|
||||
case tp == nil:
|
||||
return g.tripCh(y, x, ch), false
|
||||
case p.On(SenseMonsters) && tp.On(Invisible):
|
||||
if g.DoorStop && !g.Firstmove {
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
return ch, true
|
||||
default:
|
||||
if s.wakeup {
|
||||
g.wakeMonster(y, x)
|
||||
}
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
if p.On(Hallucinating) {
|
||||
return g.randomMonsterLetter(), false
|
||||
}
|
||||
|
||||
return tp.Disguise, false
|
||||
}
|
||||
|
||||
return ch, false
|
||||
}
|
||||
}
|
||||
|
||||
// lookRunCheck decides whether what this square shows should stop a run
|
||||
// (the DoorStop tail of the misc.c look loop). Squares on the running
|
||||
// edge are ignored.
|
||||
func (g *RogueGame) lookRunCheck(s *lookScan, ch byte, y, x int) {
|
||||
if s.atRunEdge(g.RunCh, y, x) {
|
||||
return
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case Door:
|
||||
if x == s.hero.X || y == s.hero.Y {
|
||||
g.Running = false
|
||||
}
|
||||
case Passage:
|
||||
if x == s.hero.X || y == s.hero.Y {
|
||||
s.passcount++
|
||||
}
|
||||
case Floor, '|', '-', ' ':
|
||||
default:
|
||||
g.Running = false
|
||||
}
|
||||
}
|
||||
|
||||
// atRunEdge reports whether (y, x) sits on the leading edge of the run
|
||||
// direction, where door-stop checking does not apply (the first RunCh
|
||||
// switch of the misc.c look loop).
|
||||
func (s *lookScan) atRunEdge(runCh byte, y, x int) bool {
|
||||
switch runCh {
|
||||
case 'h':
|
||||
return x == s.ex
|
||||
case 'j':
|
||||
return y == s.sy
|
||||
case 'k':
|
||||
return y == s.ey
|
||||
case 'l':
|
||||
return x == s.sx
|
||||
case 'y':
|
||||
return (y+x)-s.sumhero >= 1
|
||||
case 'u':
|
||||
return (y-x)-s.diffhero >= 1
|
||||
case 'n':
|
||||
return (y+x)-s.sumhero <= -1
|
||||
case 'b':
|
||||
return (y-x)-s.diffhero <= -1
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// tripCh returns the character for this space, taking into account whether
|
||||
// or not the player is tripping (misc.c trip_ch).
|
||||
func (g *RogueGame) tripCh(y, x int, ch byte) byte {
|
||||
if g.Player.On(Hallucinating) && g.After {
|
||||
switch ch {
|
||||
case Floor, ' ', Passage, '-', '|', Door, Trap:
|
||||
default:
|
||||
if y != g.Level.Stairs.Y || x != g.Level.Stairs.X || !g.SeenStairs {
|
||||
ch = g.rndThing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// eraseLamp erases the area shown by a lamp in a dark room
|
||||
// (misc.c erase_lamp).
|
||||
func (g *RogueGame) eraseLamp(pos Coord, rp *Room) {
|
||||
if !g.Options.SeeFloor || rp.Flags&(Gone|Dark) != Dark ||
|
||||
g.Player.On(Blind) {
|
||||
return
|
||||
}
|
||||
|
||||
ey := pos.Y + 1
|
||||
ex := pos.X + 1
|
||||
|
||||
sy := pos.Y - 1
|
||||
for x := pos.X - 1; x <= ex; x++ {
|
||||
for y := sy; y <= ey; y++ {
|
||||
if y == g.Player.Pos.Y && x == g.Player.Pos.X {
|
||||
continue
|
||||
}
|
||||
|
||||
g.move(y, x)
|
||||
|
||||
if g.inch() == Floor {
|
||||
g.addch(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// showFloor reports whether we show the floor in her room at this time
|
||||
// (misc.c show_floor).
|
||||
func (g *RogueGame) showFloor() bool {
|
||||
if g.Player.Room.Flags&(Gone|Dark) == Dark && !g.Player.On(Blind) {
|
||||
return g.Options.SeeFloor
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// eat lets her try to eat something (misc.c eat).
|
||||
func (g *RogueGame) eat() {
|
||||
obj, ok := g.promptPackItem("eat", KindFood)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindFood {
|
||||
if !g.Options.Terse {
|
||||
g.msg("ugh, you would get ill if you ate that")
|
||||
} else {
|
||||
g.msg("that's Inedible!")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
if p.FoodLeft < 0 {
|
||||
p.FoodLeft = 0
|
||||
}
|
||||
|
||||
if p.FoodLeft += HungerTime - 200 + g.rnd(400); p.FoodLeft > StomachSize {
|
||||
p.FoodLeft = StomachSize
|
||||
}
|
||||
|
||||
p.HungryState = 0
|
||||
if obj == p.CurWeapon {
|
||||
p.CurWeapon = nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case obj.Which == 1:
|
||||
g.msg("my, that was a yummy %s", g.Fruit)
|
||||
case g.rnd(100) > 70:
|
||||
p.Stats.Exp++
|
||||
|
||||
g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk"))
|
||||
g.checkLevel()
|
||||
default:
|
||||
g.msg("%s, that tasted good", g.chooseStr("oh, wow", "yum"))
|
||||
}
|
||||
|
||||
g.leavePack(obj, false, false)
|
||||
}
|
||||
|
||||
// checkLevel checks to see if the guy has gone up a level (misc.c
|
||||
// check_level).
|
||||
func (g *RogueGame) checkLevel() {
|
||||
p := &g.Player
|
||||
|
||||
var i int
|
||||
for i = 0; g.data.eLevels[i] != 0; i++ {
|
||||
if g.data.eLevels[i] > p.Stats.Exp {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
olevel := p.Stats.Lvl
|
||||
|
||||
p.Stats.Lvl = i
|
||||
if i > olevel {
|
||||
add := g.roll(i-olevel, 10)
|
||||
p.Stats.MaxHP += add
|
||||
p.Stats.HP += add
|
||||
|
||||
g.msg("welcome to level %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// changeStrength modifies the player's strength, keeping track of the
|
||||
// highest it has been (misc.c chg_str).
|
||||
func (g *RogueGame) changeStrength(amt int) {
|
||||
if amt == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
addStr(&p.Stats.Str, amt)
|
||||
|
||||
comp := p.Stats.Str
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&comp, -p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&comp, -p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
if comp > p.MaxStats.Str {
|
||||
p.MaxStats.Str = comp
|
||||
}
|
||||
}
|
||||
|
||||
// addStr performs the actual strength add, checking bounds (misc.c add_str).
|
||||
func addStr(sp *int, amt int) {
|
||||
if *sp += amt; *sp < 3 {
|
||||
*sp = 3
|
||||
} else if *sp > 31 {
|
||||
*sp = 31
|
||||
}
|
||||
}
|
||||
|
||||
// addHaste adds a haste to the player (misc.c add_haste).
|
||||
func (g *RogueGame) addHaste(potion bool) bool {
|
||||
p := &g.Player
|
||||
if p.On(Hasted) {
|
||||
g.NoCommand += g.rnd(8)
|
||||
|
||||
p.Flags.Clear(Awake | Hasted)
|
||||
g.Extinguish(DNohaste)
|
||||
g.msg("you faint from exhaustion")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
p.Flags.Set(Hasted)
|
||||
|
||||
if potion {
|
||||
g.Fuse(DNohaste, 0, g.rnd(4)+4, After)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// aggravate aggravates all the monsters on this level (misc.c aggravate).
|
||||
func (g *RogueGame) aggravate() {
|
||||
// runTo() can splice the monster list while we walk it, so iterate a copy.
|
||||
monsters := append([]*Monster(nil), g.Level.Monsters...)
|
||||
for _, mp := range monsters {
|
||||
g.runTo(mp.Pos)
|
||||
}
|
||||
}
|
||||
|
||||
// vowelstr returns "n" if the string starts with a vowel, for "a"/"an"
|
||||
// (misc.c vowelstr).
|
||||
func vowelstr(str string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch str[0] {
|
||||
case 'a', 'A', 'e', 'E', 'i', 'I', 'o', 'O', 'u', 'U':
|
||||
return "n"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// isCurrent sees if the object is one of the currently used items
|
||||
// (misc.c is_current).
|
||||
func (g *RogueGame) isCurrent(obj *Object) bool {
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
if obj == p.CurArmor || obj == p.CurWeapon ||
|
||||
obj == p.CurRing[Left] || obj == p.CurRing[Right] {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("That's already ")
|
||||
}
|
||||
|
||||
g.msg("in use")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// promptDirection sets up the direction coordinate for use in various
|
||||
// "prefix" commands (misc.c get_dir).
|
||||
func (g *RogueGame) promptDirection() bool {
|
||||
if g.Again && g.LastDir != 0 {
|
||||
g.Delta = g.lastDelt
|
||||
g.DirCh = g.LastDir
|
||||
} else {
|
||||
prompt := "direction: "
|
||||
if !g.Options.Terse {
|
||||
prompt = "which direction? "
|
||||
g.msg("%s", prompt)
|
||||
}
|
||||
|
||||
for {
|
||||
g.DirCh = g.readchar()
|
||||
if g.DirCh == Escape {
|
||||
g.LastDir = 0
|
||||
g.resetLast()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if d, ok := deltaFor(g.DirCh); ok {
|
||||
g.Delta = d
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("%s", prompt)
|
||||
}
|
||||
|
||||
g.DirCh = toLower(g.DirCh)
|
||||
g.LastDir = g.DirCh
|
||||
g.lastDelt = g.Delta
|
||||
}
|
||||
|
||||
if g.Player.On(Confused) && g.rnd(5) == 0 {
|
||||
g.confuseDirection()
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// confuseDirection randomizes the chosen direction for a confused hero
|
||||
// (the ISHUH tail of misc.c get_dir).
|
||||
func (g *RogueGame) confuseDirection() {
|
||||
for {
|
||||
g.Delta.Y = g.rnd(3) - 1
|
||||
|
||||
g.Delta.X = g.rnd(3) - 1
|
||||
if g.Delta.Y != 0 || g.Delta.X != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deltaFor maps a direction key to its movement delta; ok is false for
|
||||
// keys that are not directions (the switch of misc.c get_dir).
|
||||
func deltaFor(ch byte) (Coord, bool) {
|
||||
switch ch {
|
||||
case 'h', 'H':
|
||||
return Coord{X: -1, Y: 0}, true
|
||||
case 'j', 'J':
|
||||
return Coord{X: 0, Y: 1}, true
|
||||
case 'k', 'K':
|
||||
return Coord{X: 0, Y: -1}, true
|
||||
case 'l', 'L':
|
||||
return Coord{X: 1, Y: 0}, true
|
||||
case 'y', 'Y':
|
||||
return Coord{X: -1, Y: -1}, true
|
||||
case 'u', 'U':
|
||||
return Coord{X: 1, Y: -1}, true
|
||||
case 'b', 'B':
|
||||
return Coord{X: -1, Y: 1}, true
|
||||
case 'n', 'N':
|
||||
return Coord{X: 1, Y: 1}, true
|
||||
}
|
||||
|
||||
return Coord{}, false
|
||||
}
|
||||
|
||||
// callIt calls an object something after use (misc.c call_it).
|
||||
func (g *RogueGame) callIt(info *ObjInfo) {
|
||||
if info.Know {
|
||||
info.Guess = ""
|
||||
} else if info.Guess == "" {
|
||||
g.msg("%s", g.chooseTerse("call it: ", "what do you want to call it? "))
|
||||
|
||||
buf := ""
|
||||
if g.getStr(&buf, g.scr.Std) == Norm {
|
||||
if buf != "" {
|
||||
info.Guess = buf
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rndThing picks a random thing appropriate for this level (misc.c
|
||||
// rnd_thing).
|
||||
func (g *RogueGame) rndThing() byte {
|
||||
var i int
|
||||
if g.Depth >= AmuletLevel {
|
||||
i = g.rnd(len(g.data.thingList))
|
||||
} else {
|
||||
i = g.rnd(len(g.data.thingList) - 1)
|
||||
}
|
||||
|
||||
return g.data.thingList[i]
|
||||
}
|
||||
|
||||
// chooseStr picks the first or second string depending on whether the
|
||||
// player is tripping (misc.c choose_str).
|
||||
func (g *RogueGame) chooseStr(ts, ns string) string {
|
||||
if g.Player.On(Hallucinating) {
|
||||
return ts
|
||||
}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
// unctrl gives a printable representation of a character, like curses
|
||||
// unctrl(): control characters display as ^X.
|
||||
func unctrl(ch byte) string {
|
||||
if ch < ' ' {
|
||||
return "^" + string(ch+'@')
|
||||
}
|
||||
|
||||
if ch == 0x7f {
|
||||
return "^?"
|
||||
}
|
||||
|
||||
return string(ch)
|
||||
}
|
||||
240
game/monsters.go
240
game/monsters.go
@@ -1,240 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// monsters.c — monster creation and saving throws.
|
||||
|
||||
// randMonster picks a monster to show up; the lower the level, the meaner
|
||||
// the monster (monsters.c randmonster).
|
||||
func (g *RogueGame) randMonster(wander bool) byte {
|
||||
mons := &g.data.lvlMons
|
||||
if wander {
|
||||
mons = &g.data.wandMons
|
||||
}
|
||||
|
||||
for {
|
||||
d := g.Depth + (g.rnd(10) - 6)
|
||||
if d < 0 {
|
||||
d = g.rnd(5)
|
||||
}
|
||||
|
||||
if d > 25 {
|
||||
d = g.rnd(5) + 21
|
||||
}
|
||||
|
||||
if mons[d] != 0 {
|
||||
return mons[d]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// newMonster picks a new monster and adds it to the list (monsters.c
|
||||
// new_monster).
|
||||
func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
|
||||
levAdd := max(g.Depth-AmuletLevel, 0)
|
||||
|
||||
g.Level.AddMonster(tp)
|
||||
tp.Type = typ
|
||||
tp.Disguise = typ
|
||||
tp.Pos = cp
|
||||
g.move(cp.Y, cp.X)
|
||||
tp.OldCh = g.inch()
|
||||
tp.Room = g.roomIn(cp)
|
||||
g.Level.SetMonsterAt(cp.Y, cp.X, tp)
|
||||
mp := &g.Monsters[tp.Type-'A']
|
||||
tp.Stats.Lvl = mp.Stats.Lvl + levAdd
|
||||
tp.Stats.MaxHP = g.roll(tp.Stats.Lvl, 8)
|
||||
tp.Stats.HP = tp.Stats.MaxHP
|
||||
tp.Stats.ArmorClass = mp.Stats.ArmorClass - levAdd
|
||||
tp.Stats.Dmg = mp.Stats.Dmg
|
||||
tp.Stats.Str = mp.Stats.Str
|
||||
tp.Stats.Exp = mp.Stats.Exp + levAdd*10 + expAdd(tp)
|
||||
|
||||
tp.Flags = mp.Flags
|
||||
if g.Depth > 29 {
|
||||
tp.Flags.Set(Hasted)
|
||||
}
|
||||
|
||||
tp.Turn = true
|
||||
tp.Pack = nil
|
||||
|
||||
if g.Player.IsWearing(RingAggravateMonsters) {
|
||||
g.runTo(cp)
|
||||
}
|
||||
|
||||
if typ == 'X' {
|
||||
tp.Disguise = g.rndThing()
|
||||
}
|
||||
}
|
||||
|
||||
// expAdd is the experience to add for this monster's level/hit points
|
||||
// (monsters.c exp_add).
|
||||
func expAdd(tp *Monster) int {
|
||||
var mod int
|
||||
if tp.Stats.Lvl == 1 {
|
||||
mod = tp.Stats.MaxHP / 8
|
||||
} else {
|
||||
mod = tp.Stats.MaxHP / 6
|
||||
}
|
||||
|
||||
if tp.Stats.Lvl > 9 {
|
||||
mod *= 20
|
||||
} else if tp.Stats.Lvl > 6 {
|
||||
mod *= 4
|
||||
}
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
// wanderer creates a new wandering monster and aims it at the player
|
||||
// (monsters.c wanderer).
|
||||
func (g *RogueGame) wanderer() {
|
||||
tp := &Monster{}
|
||||
|
||||
var cp Coord
|
||||
for {
|
||||
cp, _ = g.findFloor(true)
|
||||
if g.roomIn(cp) != g.Player.Room {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
g.newMonster(tp, g.randMonster(true), cp)
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
g.standout()
|
||||
|
||||
if !g.Player.On(Hallucinating) {
|
||||
g.addch(tp.Type)
|
||||
} else {
|
||||
g.addch(g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
g.standend()
|
||||
}
|
||||
|
||||
g.runTo(tp.Pos)
|
||||
}
|
||||
|
||||
// wakeMonster is what to do when the hero steps next to a monster
|
||||
// (monsters.c wake_monster).
|
||||
func (g *RogueGame) wakeMonster(y, x int) {
|
||||
p := &g.Player
|
||||
|
||||
tp := g.Level.MonsterAt(y, x)
|
||||
if tp == nil {
|
||||
panic("can't find monster in wake_monster")
|
||||
}
|
||||
|
||||
// Every time he sees a mean monster, it might start chasing him
|
||||
if g.meanWakes(tp) {
|
||||
tp.Dest = &p.Pos
|
||||
tp.Flags.Set(Awake)
|
||||
}
|
||||
|
||||
if g.medusaCatches(tp) {
|
||||
g.medusaGaze(tp, y, x)
|
||||
}
|
||||
// Let greedy ones guard gold
|
||||
if tp.On(Greedy) && !tp.On(Awake) {
|
||||
tp.Flags.Set(Awake)
|
||||
|
||||
if p.Room.GoldVal != 0 {
|
||||
tp.Dest = &p.Room.Gold
|
||||
} else {
|
||||
tp.Dest = &p.Pos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// meanWakes decides whether a sleeping mean monster starts the chase
|
||||
// (monsters.c wake_monster). The waking roll happens for any sleeping
|
||||
// monster, as in C.
|
||||
func (g *RogueGame) meanWakes(tp *Monster) bool {
|
||||
p := &g.Player
|
||||
|
||||
return !tp.On(Awake) && g.rnd(3) != 0 && tp.On(Mean) && !tp.On(Held) &&
|
||||
!p.IsWearing(RingStealth) && !p.On(Levitating)
|
||||
}
|
||||
|
||||
// medusaCatches reports an uncovered, awake medusa the hero can see
|
||||
// (monsters.c wake_monster).
|
||||
func (g *RogueGame) medusaCatches(tp *Monster) bool {
|
||||
p := &g.Player
|
||||
|
||||
return tp.Type == 'M' && !p.On(Blind) && !p.On(Hallucinating) &&
|
||||
!tp.On(Found) && !tp.On(Cancelled) && tp.On(Awake)
|
||||
}
|
||||
|
||||
// medusaGaze confuses the hero when the medusa's gaze lands (the M
|
||||
// block of monsters.c wake_monster).
|
||||
func (g *RogueGame) medusaGaze(tp *Monster, y, x int) {
|
||||
p := &g.Player
|
||||
|
||||
rp := p.Room
|
||||
if (rp == nil || rp.Flags.Has(Dark)) &&
|
||||
distance(y, x, p.Pos.Y, p.Pos.X) >= LampDist {
|
||||
return
|
||||
}
|
||||
|
||||
tp.Flags.Set(Found)
|
||||
|
||||
if g.save(VsMagic) {
|
||||
return
|
||||
}
|
||||
|
||||
if p.On(Confused) {
|
||||
g.Lengthen(DUnconfuse, g.spread(HuhDuration))
|
||||
} else {
|
||||
g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After)
|
||||
}
|
||||
|
||||
p.Flags.Set(Confused)
|
||||
|
||||
mname := g.setMname(tp)
|
||||
g.addmsgf("%s", mname)
|
||||
|
||||
if mname != "it" {
|
||||
g.addmsgf("'")
|
||||
}
|
||||
|
||||
g.msg("s gaze has confused you")
|
||||
}
|
||||
|
||||
// givePack gives a pack to a monster if it deserves one (monsters.c
|
||||
// give_pack).
|
||||
func (g *RogueGame) givePack(tp *Monster) {
|
||||
if g.Depth >= g.MaxDepth && g.rnd(100) < g.Monsters[tp.Type-'A'].Carry {
|
||||
attachObj(&tp.Pack, g.newThing())
|
||||
}
|
||||
}
|
||||
|
||||
// saveThrow sees if a creature saves against something (monsters.c
|
||||
// save_throw).
|
||||
func (g *RogueGame) saveThrow(which int, st *Stats) bool {
|
||||
need := 14 + which - st.Lvl/2
|
||||
|
||||
return g.roll(1, 20) >= need
|
||||
}
|
||||
|
||||
// save sees if the hero saves against various nasty things (monsters.c
|
||||
// save).
|
||||
func (g *RogueGame) save(which int) bool {
|
||||
p := &g.Player
|
||||
if which == VsMagic {
|
||||
if p.IsRing(Left, RingProtection) {
|
||||
which -= p.CurRing[Left].Bonus
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingProtection) {
|
||||
which -= p.CurRing[Right].Bonus
|
||||
}
|
||||
}
|
||||
|
||||
return g.saveThrow(which, &p.Stats)
|
||||
}
|
||||
|
||||
// randomMonsterLetter picks a random monster display letter, used by the
|
||||
// hallucination effects (the C rnd(26)+'A' idiom).
|
||||
func (g *RogueGame) randomMonsterLetter() byte {
|
||||
return byte(g.rnd(26) + 'A') //nolint:gosec // G115: 'A'..'Z' fits a byte
|
||||
}
|
||||
502
game/move.go
502
game/move.go
@@ -1,502 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// move.c — hero movement commands.
|
||||
|
||||
// startRun starts the hero running (move.c do_run).
|
||||
func (g *RogueGame) startRun(ch byte) {
|
||||
g.Running = true
|
||||
g.After = false
|
||||
g.RunCh = ch
|
||||
}
|
||||
|
||||
// moveHero checks that a move is legal and handles the consequences —
|
||||
// fighting, picking up, etc. (move.c do_move). The C `goto over`
|
||||
// re-check after a passage turn is the retry loop.
|
||||
func (g *RogueGame) moveHero(dy, dx int) {
|
||||
p := &g.Player
|
||||
|
||||
g.Firstmove = false
|
||||
if g.NoMove > 0 {
|
||||
g.NoMove--
|
||||
g.msg("you are still stuck in the bear trap")
|
||||
|
||||
return
|
||||
}
|
||||
// Do a confused move (maybe)
|
||||
var nh Coord
|
||||
if p.On(Confused) && g.rnd(5) != 0 {
|
||||
nh = g.randomStep(&p.Creature)
|
||||
if nh == p.Pos {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
g.ToDeath = false
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
|
||||
}
|
||||
|
||||
for {
|
||||
ch, fl, stop := g.moveTarget(nh)
|
||||
if stop {
|
||||
return
|
||||
}
|
||||
|
||||
turned, ndy, ndx := g.moveResolve(nh, ch, fl, dy, dx)
|
||||
if !turned {
|
||||
return
|
||||
}
|
||||
|
||||
// the C goto over: re-check the turned move
|
||||
dy, dx = ndy, ndx
|
||||
|
||||
g.turnRefresh()
|
||||
|
||||
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
|
||||
}
|
||||
}
|
||||
|
||||
// moveResolve acts on the square the hero stepped at: a wall may turn a
|
||||
// passage runner (reported with the new deltas); anything else completes
|
||||
// or refuses the move (the switch of move.c do_move).
|
||||
func (g *RogueGame) moveResolve(
|
||||
nh Coord, ch byte, fl PlaceFlags, dy, dx int,
|
||||
) (bool, int, int) {
|
||||
switch ch {
|
||||
case ' ', '|', '-':
|
||||
if turn, ndy, ndx := g.passageTurn(dy, dx); turn {
|
||||
return true, ndy, ndx
|
||||
}
|
||||
|
||||
g.Running = false
|
||||
g.After = false
|
||||
default:
|
||||
g.moveEnter(nh, fl, ch)
|
||||
}
|
||||
|
||||
return false, 0, 0
|
||||
}
|
||||
|
||||
// moveEnter completes a step onto a walkable square: doors, traps,
|
||||
// passages, floor, and things (the entry arms of the move.c do_move
|
||||
// switch).
|
||||
func (g *RogueGame) moveEnter(nh Coord, fl PlaceFlags, ch byte) {
|
||||
p := &g.Player
|
||||
|
||||
switch ch {
|
||||
case Door:
|
||||
g.Running = false
|
||||
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPassage) {
|
||||
g.enterRoom(nh)
|
||||
}
|
||||
case Trap:
|
||||
tr := g.springTrap(nh)
|
||||
if tr == TrapDoor || tr == TrapTeleport {
|
||||
return
|
||||
}
|
||||
case Passage:
|
||||
// when you're in a corridor, you don't know if you're in a maze
|
||||
// room or not, and there ain't no way to find out if you're
|
||||
// leaving a maze room, so it is necessary to always recalculate
|
||||
// proom.
|
||||
p.Room = g.roomIn(p.Pos)
|
||||
case Floor:
|
||||
if !fl.Has(FReal) {
|
||||
g.springTrap(p.Pos)
|
||||
}
|
||||
default:
|
||||
g.moveOnto(nh, fl, ch)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.finishMove(nh, fl)
|
||||
}
|
||||
|
||||
// offMap reports coordinates outside the walkable map (move.c do_move).
|
||||
func offMap(nh Coord) bool {
|
||||
return nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
|
||||
}
|
||||
|
||||
// moveTarget inspects the square the hero is stepping onto: bounds and
|
||||
// diagonal legality, hidden traps underfoot, and being held. stop means
|
||||
// the move is refused (the checks of move.c do_move).
|
||||
func (g *RogueGame) moveTarget(nh Coord) (byte, PlaceFlags, bool) {
|
||||
p := &g.Player
|
||||
|
||||
// Check if he tried to move off the screen or make an illegal
|
||||
// diagonal move, and stop him if he did.
|
||||
if offMap(nh) {
|
||||
return ' ', 0, false // fall into the wall case
|
||||
}
|
||||
|
||||
if !g.diagOk(p.Pos, nh) {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
|
||||
return 0, 0, true
|
||||
}
|
||||
|
||||
if g.Running && p.Pos == nh {
|
||||
g.After = false
|
||||
g.Running = false
|
||||
}
|
||||
|
||||
fl := *g.Level.FlagsAt(nh.Y, nh.X)
|
||||
|
||||
ch := g.Level.VisibleChar(nh.Y, nh.X)
|
||||
if !fl.Has(FReal) && ch == Floor {
|
||||
if !p.On(Levitating) {
|
||||
ch = Trap
|
||||
g.Level.SetChar(nh.Y, nh.X, Trap)
|
||||
g.Level.FlagsAt(nh.Y, nh.X).Set(FReal)
|
||||
}
|
||||
} else if p.On(Held) && ch != 'F' {
|
||||
g.msg("you are being held")
|
||||
|
||||
return 0, 0, true
|
||||
}
|
||||
|
||||
return ch, fl, false
|
||||
}
|
||||
|
||||
// moveOnto handles stepping at a monster or onto an item (the default
|
||||
// arm of the move.c do_move switch).
|
||||
func (g *RogueGame) moveOnto(nh Coord, fl PlaceFlags, ch byte) {
|
||||
p := &g.Player
|
||||
if ch == Stairs {
|
||||
g.SeenStairs = true
|
||||
}
|
||||
|
||||
g.Running = false
|
||||
if isUpper(ch) || g.Level.MonsterAt(nh.Y, nh.X) != nil {
|
||||
g.fight(nh, p.CurWeapon, false)
|
||||
} else {
|
||||
if ch != Stairs {
|
||||
g.Take = ch
|
||||
}
|
||||
|
||||
g.finishMove(nh, fl)
|
||||
}
|
||||
}
|
||||
|
||||
// passageTurn checks whether a runner in a gone-room passage should turn
|
||||
// the corner instead of stopping at a wall (the PASSGO block of move.c
|
||||
// do_move). It reports whether to turn and the new deltas, updating RunCh.
|
||||
func (g *RogueGame) passageTurn(dy, dx int) (bool, int, int) {
|
||||
p := &g.Player
|
||||
if !g.Options.PassGo || !g.Running || !p.Room.Flags.Has(Gone) ||
|
||||
p.On(Blind) {
|
||||
return false, dy, dx
|
||||
}
|
||||
|
||||
switch g.RunCh {
|
||||
case 'h', 'l':
|
||||
if turn, ndy := g.passageTurnVertical(); turn {
|
||||
return true, ndy, 0
|
||||
}
|
||||
case 'j', 'k':
|
||||
if turn, ndx := g.passageTurnHorizontal(); turn {
|
||||
return true, 0, ndx
|
||||
}
|
||||
}
|
||||
|
||||
return false, dy, dx
|
||||
}
|
||||
|
||||
// passageTurnVertical decides whether a horizontal runner turns up or
|
||||
// down at a corner (move.c do_move).
|
||||
func (g *RogueGame) passageTurnVertical() (bool, int) {
|
||||
p := &g.Player
|
||||
|
||||
b1 := p.Pos.Y != 1 && g.turnOk(p.Pos.Y-1, p.Pos.X)
|
||||
|
||||
b2 := p.Pos.Y != NumLines-2 && g.turnOk(p.Pos.Y+1, p.Pos.X)
|
||||
if b1 == b2 {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if b1 {
|
||||
g.RunCh = 'k'
|
||||
|
||||
return true, -1
|
||||
}
|
||||
|
||||
g.RunCh = 'j'
|
||||
|
||||
return true, 1
|
||||
}
|
||||
|
||||
// passageTurnHorizontal decides whether a vertical runner turns left or
|
||||
// right at a corner (move.c do_move).
|
||||
func (g *RogueGame) passageTurnHorizontal() (bool, int) {
|
||||
p := &g.Player
|
||||
|
||||
b1 := p.Pos.X != 0 && g.turnOk(p.Pos.Y, p.Pos.X-1)
|
||||
|
||||
b2 := p.Pos.X != NumCols-1 && g.turnOk(p.Pos.Y, p.Pos.X+1)
|
||||
if b1 == b2 {
|
||||
return false, 0
|
||||
}
|
||||
|
||||
if b1 {
|
||||
g.RunCh = 'h'
|
||||
|
||||
return true, -1
|
||||
}
|
||||
|
||||
g.RunCh = 'l'
|
||||
|
||||
return true, 1
|
||||
}
|
||||
|
||||
// finishMove is the move_stuff label in do_move: complete the step.
|
||||
func (g *RogueGame) finishMove(nh Coord, fl PlaceFlags) {
|
||||
p := &g.Player
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
|
||||
|
||||
if fl.Has(FPassage) && g.Level.Char(g.Oldpos.Y, g.Oldpos.X) == Door {
|
||||
g.leaveRoom(nh)
|
||||
}
|
||||
|
||||
p.Pos = nh
|
||||
}
|
||||
|
||||
// turnOk decides whether it is legal to turn onto the given space
|
||||
// (move.c turn_ok).
|
||||
func (g *RogueGame) turnOk(y, x int) bool {
|
||||
pp := g.Level.At(y, x)
|
||||
|
||||
return pp.Ch == Door || pp.Flags&(FReal|FPassage) == (FReal|FPassage)
|
||||
}
|
||||
|
||||
// turnRefresh decides whether to refresh at a passage turning (move.c
|
||||
// turnref).
|
||||
func (g *RogueGame) turnRefresh() {
|
||||
p := &g.Player
|
||||
|
||||
pp := g.Level.At(p.Pos.Y, p.Pos.X)
|
||||
if !pp.Flags.Has(FSeen) {
|
||||
if g.Options.Jump {
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen)
|
||||
}
|
||||
}
|
||||
|
||||
// doorOpen is called to wake up things in a room that might move when the
|
||||
// hero enters (move.c door_open).
|
||||
func (g *RogueGame) doorOpen(rp *Room) {
|
||||
if rp.Flags.Has(Gone) {
|
||||
return
|
||||
}
|
||||
|
||||
for y := rp.Pos.Y; y < rp.Pos.Y+rp.Max.Y; y++ {
|
||||
for x := rp.Pos.X; x < rp.Pos.X+rp.Max.X; x++ {
|
||||
if isUpper(g.Level.VisibleChar(y, x)) {
|
||||
g.wakeMonster(y, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// springTrap makes him pay for stepping on a trap (move.c be_trapped).
|
||||
func (g *RogueGame) springTrap(tc Coord) TrapKind {
|
||||
p := &g.Player
|
||||
if p.On(Levitating) {
|
||||
return TrapRust // anything that's not a door or teleport
|
||||
}
|
||||
|
||||
g.Running = false
|
||||
g.Count = 0
|
||||
pp := g.Level.At(tc.Y, tc.X)
|
||||
pp.Ch = Trap
|
||||
tr := TrapKind(pp.Flags & FTrapMask)
|
||||
pp.Flags.Set(FSeen)
|
||||
|
||||
if h := g.data.trapHandlers[tr]; h != nil {
|
||||
h(g, tc)
|
||||
}
|
||||
|
||||
g.flushType()
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
// The per-trap effect handlers, dispatched through
|
||||
// gameData.trapHandlers. Each is one case of the C be_trapped switch.
|
||||
|
||||
func (g *RogueGame) trapFall(Coord) {
|
||||
g.Depth++
|
||||
g.NewLevel()
|
||||
g.msg("you fell into a trap!")
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapBear(Coord) {
|
||||
g.NoMove += g.spread(3) // BEARTIME
|
||||
g.msg("you are caught in a bear trap")
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapMystery(Coord) {
|
||||
which := g.rnd(11)
|
||||
switch which {
|
||||
case 0:
|
||||
g.msg("you are suddenly in a parallel dimension")
|
||||
case 1:
|
||||
g.msg("the light in here suddenly seems %s",
|
||||
g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
case 2:
|
||||
g.msg("you feel a sting in the side of your neck")
|
||||
case 3:
|
||||
g.msg("multi-colored lines swirl around you, then fade")
|
||||
case 4:
|
||||
g.msg("a %s light flashes in your eyes", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
case 5:
|
||||
g.msg("a spike shoots past your ear!")
|
||||
default:
|
||||
g.trapMysteryMore(which)
|
||||
}
|
||||
}
|
||||
|
||||
// trapMysteryMore holds the back half of the mystery-trap messages.
|
||||
func (g *RogueGame) trapMysteryMore(which int) {
|
||||
switch which {
|
||||
case 6:
|
||||
g.msg("%s sparks dance across your armor", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
case 7:
|
||||
g.msg("you suddenly feel very thirsty")
|
||||
case 8:
|
||||
g.msg("you feel time speed up suddenly")
|
||||
case 9:
|
||||
g.msg("time now seems to be going slower")
|
||||
case 10:
|
||||
g.msg("you pack turns %s!", g.data.rainbow[g.rnd(len(g.data.rainbow))])
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapSleep(Coord) {
|
||||
g.NoCommand += g.spread(5) // SLEEPTIME
|
||||
|
||||
g.Player.Flags.Clear(Awake)
|
||||
g.msg("a strange white mist envelops you and you fall asleep")
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapArrow(Coord) {
|
||||
p := &g.Player
|
||||
if g.swing(p.Stats.Lvl-1, p.Stats.ArmorClass, 1) {
|
||||
p.Stats.HP -= g.roll(1, 6)
|
||||
if p.Stats.HP <= 0 {
|
||||
g.msg("an arrow killed you")
|
||||
g.death('a')
|
||||
} else {
|
||||
g.msg("oh no! An arrow shot you")
|
||||
}
|
||||
} else {
|
||||
arrow := newObject()
|
||||
g.initWeapon(arrow, WeaponArrow)
|
||||
arrow.Count = 1
|
||||
arrow.Pos = p.Pos
|
||||
g.fall(arrow, false)
|
||||
g.msg("an arrow shoots past you")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapTeleport(tc Coord) {
|
||||
// since the hero's leaving, look() won't put a TRAP down for us,
|
||||
// so we have to do it ourself
|
||||
g.teleport()
|
||||
g.mvaddch(tc.Y, tc.X, Trap)
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapDart(Coord) {
|
||||
p := &g.Player
|
||||
if !g.swing(p.Stats.Lvl+1, p.Stats.ArmorClass, 1) {
|
||||
g.msg("a small dart whizzes by your ear and vanishes")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.Stats.HP -= g.roll(1, 4)
|
||||
if p.Stats.HP <= 0 {
|
||||
g.msg("a poisoned dart killed you")
|
||||
g.death('d')
|
||||
}
|
||||
|
||||
if !p.IsWearing(RingSustainStrength) && !g.save(VsPoison) {
|
||||
g.changeStrength(-1)
|
||||
}
|
||||
|
||||
g.msg("a small dart just hit you in the shoulder")
|
||||
}
|
||||
|
||||
func (g *RogueGame) trapRust(Coord) {
|
||||
g.msg("a gush of water hits you on the head")
|
||||
g.rustArmor(g.Player.CurArmor)
|
||||
}
|
||||
|
||||
// randomStep moves in a random direction if the monster/person is
|
||||
// confused (move.c rndmove).
|
||||
func (g *RogueGame) randomStep(who *Creature) Coord {
|
||||
ret := Coord{
|
||||
Y: who.Pos.Y + g.rnd(3) - 1,
|
||||
X: who.Pos.X + g.rnd(3) - 1,
|
||||
}
|
||||
// Now check to see if that's a legal move. If not, don't move.
|
||||
// (I.e., bump into the wall or whatever)
|
||||
if ret == who.Pos {
|
||||
return ret
|
||||
}
|
||||
|
||||
if !g.diagOk(who.Pos, ret) {
|
||||
return who.Pos
|
||||
}
|
||||
|
||||
ch := g.Level.VisibleChar(ret.Y, ret.X)
|
||||
if !stepOk(ch) {
|
||||
return who.Pos
|
||||
}
|
||||
|
||||
if ch == Scroll {
|
||||
var found *Object
|
||||
|
||||
for _, obj := range g.Level.Objects {
|
||||
if ret.Y == obj.Pos.Y && ret.X == obj.Pos.X {
|
||||
found = obj
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found != nil && found.ScrollKind() == ScrollScareMonster {
|
||||
return who.Pos
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// rustArmor rusts the given armor, if it is a legal kind to rust, and we
|
||||
// aren't wearing a magic ring (move.c rust_armor).
|
||||
func (g *RogueGame) rustArmor(arm *Object) {
|
||||
if arm == nil || arm.Kind != KindArmor || arm.ArmorKind() == ArmorLeather ||
|
||||
arm.ArmorClass >= 9 {
|
||||
return
|
||||
}
|
||||
|
||||
if arm.Flags.Has(Protected) || g.Player.IsWearing(RingMaintainArmor) {
|
||||
if !g.ToDeath {
|
||||
g.msg("the rust vanishes instantly")
|
||||
}
|
||||
} else {
|
||||
arm.ArmorClass++
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.msg("your armor appears to be weaker now. Oh my!")
|
||||
} else {
|
||||
g.msg("your armor weakens")
|
||||
}
|
||||
}
|
||||
}
|
||||
164
game/newlevel.go
164
game/newlevel.go
@@ -1,164 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// new_level.c — dig and draw a new level.
|
||||
|
||||
const (
|
||||
treasRoomChance = 20 // one chance in TREAS_ROOM for a treasure room
|
||||
maxTreas = 10 // maximum number of treasures in a treasure room
|
||||
minTreas = 2 // minimum number of treasures in a treasure room
|
||||
maxTries = 10 // max number of tries to put down a monster
|
||||
)
|
||||
|
||||
// NewLevel digs and draws a new level (new_level.c new_level).
|
||||
func (g *RogueGame) NewLevel() {
|
||||
p := &g.Player
|
||||
p.Flags.Clear(Held) // unhold when you go down just in case
|
||||
|
||||
if g.Depth > g.MaxDepth {
|
||||
g.MaxDepth = g.Depth
|
||||
}
|
||||
// Clean things off from last level
|
||||
for i := range g.Level.Places {
|
||||
g.Level.Places[i] = Place{Ch: ' ', Flags: FReal}
|
||||
}
|
||||
|
||||
g.clear()
|
||||
// Free up the monsters on the last level; the objects and their packs
|
||||
// go with them (the garbage collector is our free_list).
|
||||
g.Level.Monsters = nil
|
||||
g.Level.Objects = nil
|
||||
g.digRooms() // Draw rooms
|
||||
g.digPassages() // Draw passages
|
||||
|
||||
p.NoFood++
|
||||
|
||||
g.putThings() // Place objects (if any)
|
||||
// Place the traps
|
||||
if g.rnd(10) < g.Depth {
|
||||
g.Level.TrapCount = min(g.rnd(g.Depth/4)+1, MaxTraps)
|
||||
|
||||
for i := g.Level.TrapCount; i > 0; i-- {
|
||||
// not only wouldn't it be NICE to have traps in mazes (not
|
||||
// that we care about being nice), since the trap number is
|
||||
// stored where the passage number is, we can't actually do it.
|
||||
var stairs Coord
|
||||
for {
|
||||
stairs, _ = g.findFloor(false)
|
||||
if g.Level.Char(stairs.Y, stairs.X) == Floor {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sp := g.Level.FlagsAt(stairs.Y, stairs.X)
|
||||
sp.Clear(FReal)
|
||||
*sp |= PlaceFlags(g.rnd(NumTrapTypes)) //nolint:gosec // G115: 0..7 fits
|
||||
}
|
||||
}
|
||||
// Place the staircase down.
|
||||
stairs, _ := g.findFloor(false)
|
||||
g.Level.Stairs = stairs
|
||||
g.Level.SetChar(stairs.Y, stairs.X, Stairs)
|
||||
g.SeenStairs = false
|
||||
|
||||
for _, tp := range g.Level.Monsters {
|
||||
tp.Room = g.roomIn(tp.Pos)
|
||||
}
|
||||
|
||||
hero, _ := g.findFloor(true)
|
||||
p.Pos = hero
|
||||
g.enterRoom(hero)
|
||||
g.mvaddch(hero.Y, hero.X, PlayerCh)
|
||||
|
||||
if p.On(SenseMonsters) {
|
||||
g.turnSee(false)
|
||||
}
|
||||
|
||||
if p.On(Hallucinating) {
|
||||
g.visuals(0)
|
||||
}
|
||||
}
|
||||
|
||||
// randomRoom picks a room that is really there (new_level.c rnd_room).
|
||||
func (g *RogueGame) randomRoom() int {
|
||||
for {
|
||||
rm := g.rnd(MaxRooms)
|
||||
if !g.Level.Rooms[rm].Flags.Has(Gone) {
|
||||
return rm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putThings puts potions and scrolls on this level (new_level.c
|
||||
// put_things).
|
||||
func (g *RogueGame) putThings() {
|
||||
// Once you have found the amulet, the only way to get new stuff is to
|
||||
// go down into the dungeon.
|
||||
if g.HasAmulet && g.Depth < g.MaxDepth {
|
||||
return
|
||||
}
|
||||
// check for treasure rooms, and if so, put it in.
|
||||
if g.rnd(treasRoomChance) == 0 {
|
||||
g.treasureRoom()
|
||||
}
|
||||
// Do MAXOBJ attempts to put things on a level
|
||||
for range MaxObj {
|
||||
if g.rnd(100) < 36 {
|
||||
// Pick a new object and link it in the list
|
||||
obj := g.newThing()
|
||||
g.Level.AddObject(obj)
|
||||
// Put it somewhere
|
||||
obj.Pos, _ = g.findFloor(false)
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
|
||||
}
|
||||
}
|
||||
// If he is really deep in the dungeon and he hasn't found the amulet
|
||||
// yet, put it somewhere on the ground
|
||||
if g.Depth >= AmuletLevel && !g.HasAmulet {
|
||||
obj := newObject()
|
||||
g.Level.AddObject(obj)
|
||||
obj.Damage = dice("0x0")
|
||||
obj.HurlDmg = dice("0x0")
|
||||
obj.ArmorClass = 11
|
||||
obj.Kind = KindAmulet
|
||||
// Put it somewhere
|
||||
obj.Pos, _ = g.findFloor(false)
|
||||
g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Amulet)
|
||||
}
|
||||
}
|
||||
|
||||
// treasureRoom adds a treasure room (new_level.c treas_room).
|
||||
func (g *RogueGame) treasureRoom() {
|
||||
rp := &g.Level.Rooms[g.randomRoom()]
|
||||
|
||||
spots := min((rp.Max.Y-2)*(rp.Max.X-2)-minTreas, maxTreas-minTreas)
|
||||
|
||||
numMonst := g.rnd(spots) + minTreas
|
||||
for nm := numMonst; nm > 0; nm-- {
|
||||
mp, _ := g.findFloorIn(rp, 2*maxTries, false)
|
||||
tp := g.newThing()
|
||||
tp.Pos = mp
|
||||
g.Level.AddObject(tp)
|
||||
g.Level.SetChar(mp.Y, mp.X, tp.Kind.Glyph())
|
||||
}
|
||||
|
||||
// fill up room with monsters from the next level down
|
||||
nm := max(g.rnd(spots)+minTreas, numMonst+2)
|
||||
|
||||
spots = (rp.Max.Y - 2) * (rp.Max.X - 2)
|
||||
if nm > spots {
|
||||
nm = spots
|
||||
}
|
||||
|
||||
g.Depth++
|
||||
for ; nm > 0; nm-- {
|
||||
if mp, ok := g.findFloorIn(rp, maxTries, true); ok {
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, g.randMonster(false), mp)
|
||||
tp.Flags.Set(Mean) // no sloughers in THIS room
|
||||
g.givePack(tp)
|
||||
}
|
||||
}
|
||||
|
||||
g.Depth--
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func genLevel(t *testing.T, seed int32) *RogueGame {
|
||||
t.Helper()
|
||||
|
||||
g := New(Params{Seed: seed})
|
||||
g.NewLevel()
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// renderMap draws the raw level map (not the screen) as text.
|
||||
func renderMap(g *RogueGame) string {
|
||||
var sb strings.Builder
|
||||
|
||||
for y := range NumLines {
|
||||
for x := range NumCols {
|
||||
ch := g.Level.Char(y, x)
|
||||
if m := g.Level.MonsterAt(y, x); m != nil {
|
||||
ch = m.Type
|
||||
}
|
||||
|
||||
sb.WriteByte(ch)
|
||||
}
|
||||
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestNewLevelInvariants(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, seed := range []int32{1, 12345, 2026, 99999} {
|
||||
g := genLevel(t, seed)
|
||||
|
||||
checkHeroPlacement(t, g, seed)
|
||||
checkRoomsDrawn(t, g, seed)
|
||||
checkMonstersPlaced(t, g, seed)
|
||||
checkObjectsPlaced(t, g, seed)
|
||||
checkStartingKit(t, g, seed)
|
||||
}
|
||||
}
|
||||
|
||||
// checkHeroPlacement verifies the staircase and hero landed on valid,
|
||||
// unoccupied cells.
|
||||
func checkHeroPlacement(t *testing.T, g *RogueGame, seed int32) {
|
||||
t.Helper()
|
||||
|
||||
// The staircase is somewhere real.
|
||||
st := g.Level.Stairs
|
||||
if g.Level.Char(st.Y, st.X) != Stairs {
|
||||
t.Errorf("seed %d: no staircase at recorded stairs position", seed)
|
||||
}
|
||||
|
||||
// The hero stands on a walkable, monster-free cell.
|
||||
hp := g.Player.Pos
|
||||
if !stepOk(g.Level.Char(hp.Y, hp.X)) {
|
||||
t.Errorf("seed %d: hero on unwalkable cell %q", seed,
|
||||
g.Level.Char(hp.Y, hp.X))
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(hp.Y, hp.X) != nil {
|
||||
t.Errorf("seed %d: hero standing on a monster", seed)
|
||||
}
|
||||
|
||||
if g.Player.Room == nil {
|
||||
t.Errorf("seed %d: hero not in any room", seed)
|
||||
}
|
||||
}
|
||||
|
||||
// checkRoomsDrawn verifies rooms and floor/passages appear on the map.
|
||||
func checkRoomsDrawn(t *testing.T, g *RogueGame, seed int32) {
|
||||
t.Helper()
|
||||
|
||||
m := renderMap(g)
|
||||
if !strings.Contains(m, "|") || !strings.Contains(m, "-") {
|
||||
t.Errorf("seed %d: no room walls drawn", seed)
|
||||
}
|
||||
|
||||
if !strings.Contains(m, ".") && !strings.Contains(m, "#") {
|
||||
t.Errorf("seed %d: no floor or passages drawn", seed)
|
||||
}
|
||||
}
|
||||
|
||||
// checkMonstersPlaced verifies every monster is indexed on the map and
|
||||
// placed in a room.
|
||||
func checkMonstersPlaced(t *testing.T, g *RogueGame, seed int32) {
|
||||
t.Helper()
|
||||
|
||||
for _, mon := range g.Level.Monsters {
|
||||
if g.Level.MonsterAt(mon.Pos.Y, mon.Pos.X) != mon {
|
||||
t.Errorf("seed %d: monster %c not indexed at its position",
|
||||
seed, mon.Type)
|
||||
}
|
||||
|
||||
if mon.Room == nil {
|
||||
t.Errorf("seed %d: monster %c has no room", seed, mon.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkObjectsPlaced verifies every level object sits on a cell
|
||||
// displaying its type (items can share cells only with monsters standing
|
||||
// on them).
|
||||
func checkObjectsPlaced(t *testing.T, g *RogueGame, seed int32) {
|
||||
t.Helper()
|
||||
|
||||
for _, obj := range g.Level.Objects {
|
||||
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
|
||||
if ch != obj.Kind.Glyph() &&
|
||||
g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil {
|
||||
t.Errorf("seed %d: object %v at (%d,%d) but map shows %q",
|
||||
seed, obj.Kind, obj.Pos.Y, obj.Pos.X, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkStartingKit verifies the player has her starting kit: food, armor,
|
||||
// mace, bow, arrows.
|
||||
func checkStartingKit(t *testing.T, g *RogueGame, seed int32) {
|
||||
t.Helper()
|
||||
|
||||
if len(g.Player.Pack) != 5 {
|
||||
t.Errorf("seed %d: starting pack has %d items, want 5",
|
||||
seed, len(g.Player.Pack))
|
||||
}
|
||||
|
||||
if g.Player.CurWeapon == nil ||
|
||||
g.Player.CurWeapon.WeaponKind() != WeaponMace {
|
||||
t.Errorf("seed %d: not wielding the starting mace", seed)
|
||||
}
|
||||
|
||||
if g.Player.CurArmor == nil ||
|
||||
g.Player.CurArmor.ArmorKind() != ArmorRingMail {
|
||||
t.Errorf("seed %d: not wearing the starting ring mail", seed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLevelDeterministic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a := renderMap(genLevel(t, 12345))
|
||||
|
||||
b := renderMap(genLevel(t, 12345))
|
||||
if a != b {
|
||||
t.Error("same seed produced different levels")
|
||||
}
|
||||
|
||||
c := renderMap(genLevel(t, 54321))
|
||||
if a == c {
|
||||
t.Error("different seeds produced identical levels")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeeperLevels exercises generation across many depths and seeds —
|
||||
// mazes, dark rooms, traps, treasure rooms — as a crash/invariant sweep.
|
||||
func TestDeeperLevels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, seed := range []int32{7, 42, 1000, 31337} {
|
||||
g := New(Params{Seed: seed})
|
||||
for depth := 1; depth <= 30; depth++ {
|
||||
g.Depth = depth
|
||||
g.NewLevel()
|
||||
|
||||
st := g.Level.Stairs
|
||||
if g.Level.Char(st.Y, st.X) != Stairs {
|
||||
t.Fatalf("seed %d depth %d: missing staircase", seed, depth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
261
game/object.go
261
game/object.go
@@ -1,261 +0,0 @@
|
||||
package game
|
||||
|
||||
// ObjectKind is the category of an item. In C this was the o_type byte,
|
||||
// which doubled as the character drawn on the map; the port separates the
|
||||
// category (ObjectKind) from its display character (Glyph).
|
||||
type ObjectKind int
|
||||
|
||||
// Item categories.
|
||||
const (
|
||||
KindNone ObjectKind = iota
|
||||
KindPotion
|
||||
KindScroll
|
||||
KindFood
|
||||
KindWeapon
|
||||
KindArmor
|
||||
KindRing
|
||||
KindWand // a "stick": wand or staff
|
||||
KindAmulet
|
||||
KindGold
|
||||
)
|
||||
|
||||
// Prompt-filter pseudo-kinds for item selection (rogue.h CALLABLE and
|
||||
// R_OR_S): they never appear on an Object, only as getItem filters.
|
||||
const (
|
||||
KindCallable ObjectKind = -1
|
||||
KindRingOrStick ObjectKind = -2
|
||||
)
|
||||
|
||||
// Category words shared by ObjectKind.String, the discovery list, and the
|
||||
// ident table. (The bare identifiers Potion, Scroll, Ring, Gold are the
|
||||
// glyph byte constants.)
|
||||
const (
|
||||
potionName = "potion"
|
||||
scrollName = "scroll"
|
||||
ringName = "ring"
|
||||
goldName = "gold"
|
||||
)
|
||||
|
||||
// Glyph returns the map/display character for this kind of object.
|
||||
func (k ObjectKind) Glyph() byte {
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch k {
|
||||
case KindPotion:
|
||||
return Potion
|
||||
case KindScroll:
|
||||
return Scroll
|
||||
case KindFood:
|
||||
return Food
|
||||
case KindWeapon:
|
||||
return Weapon
|
||||
case KindArmor:
|
||||
return Armor
|
||||
case KindRing:
|
||||
return Ring
|
||||
case KindWand:
|
||||
return Stick
|
||||
case KindAmulet:
|
||||
return Amulet
|
||||
case KindGold:
|
||||
return Gold
|
||||
}
|
||||
|
||||
return ' '
|
||||
}
|
||||
|
||||
// String names the category the way the C type_name() did.
|
||||
func (k ObjectKind) String() string {
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch k {
|
||||
case KindPotion:
|
||||
return potionName
|
||||
case KindScroll:
|
||||
return scrollName
|
||||
case KindFood:
|
||||
return "food"
|
||||
case KindWeapon:
|
||||
return "weapon"
|
||||
case KindArmor:
|
||||
return "suit of armor"
|
||||
case KindRing:
|
||||
return ringName
|
||||
default:
|
||||
return k.stringRest()
|
||||
}
|
||||
}
|
||||
|
||||
// objectKindForGlyph is the reverse of Glyph: what category of item does a
|
||||
// map character denote. Returns KindNone for non-item characters.
|
||||
func objectKindForGlyph(ch byte) ObjectKind {
|
||||
switch ch {
|
||||
case Potion:
|
||||
return KindPotion
|
||||
case Scroll:
|
||||
return KindScroll
|
||||
case Food:
|
||||
return KindFood
|
||||
case Weapon:
|
||||
return KindWeapon
|
||||
case Armor:
|
||||
return KindArmor
|
||||
case Ring:
|
||||
return KindRing
|
||||
case Stick:
|
||||
return KindWand
|
||||
case Amulet:
|
||||
return KindAmulet
|
||||
case Gold:
|
||||
return KindGold
|
||||
}
|
||||
|
||||
return KindNone
|
||||
}
|
||||
|
||||
// MergesInPack reports whether picking up another of this kind merges into
|
||||
// an existing pack entry's count (rogue.h ISMULT).
|
||||
func (k ObjectKind) MergesInPack() bool {
|
||||
return k == KindPotion || k == KindScroll || k == KindFood
|
||||
}
|
||||
|
||||
// stringRest names the remaining kinds, including the ring-or-stick
|
||||
// prompt pseudo-kind (the tail of the C type_name switch).
|
||||
func (k ObjectKind) stringRest() string {
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch k {
|
||||
case KindWand:
|
||||
return "wand or staff"
|
||||
case KindAmulet:
|
||||
return "amulet"
|
||||
case KindGold:
|
||||
return goldName
|
||||
case KindRingOrStick:
|
||||
return "ring, wand or staff"
|
||||
}
|
||||
|
||||
return "bizarre thing"
|
||||
}
|
||||
|
||||
// Object is the _o arm of the C THING union: anything that can lie on the
|
||||
// floor or ride in a pack.
|
||||
type Object struct {
|
||||
Kind ObjectKind // what kind of object it is (o_type)
|
||||
Pos Coord // where it lives on the screen
|
||||
Text string // what it says if you read it
|
||||
Launch WeaponKind // what you need to launch it (noWeapon if none)
|
||||
PackCh byte // what character it is in the pack
|
||||
Damage DiceSpec // damage if used like sword
|
||||
HurlDmg DiceSpec // damage if thrown
|
||||
Count int // count for plural objects
|
||||
Which int // which object of a type it is (index for the Kind's table)
|
||||
HPlus int // plusses to hit
|
||||
DPlus int // plusses to damage
|
||||
ArmorClass int // armor protection (armor only)
|
||||
Charges int // charges remaining (wands and staffs only)
|
||||
GoldValue int // worth (gold piles only)
|
||||
Bonus int // magic bonus (rings only: protection, add strength, ...)
|
||||
Flags ObjFlags
|
||||
Group int // group number for this object
|
||||
Label string // label for object
|
||||
}
|
||||
|
||||
// newObject is list.c new_item() for objects: a zeroed Object with the
|
||||
// same non-zero defaults the C code relies on.
|
||||
func newObject() *Object {
|
||||
return &Object{Launch: noWeapon}
|
||||
}
|
||||
|
||||
// whichLimit reports how many entries a kind's per-kind tables hold, so
|
||||
// Which is a legal index exactly while 0 <= Which < whichLimit(Kind).
|
||||
// Kinds whose Which is not a table index at all — food (0 ration, 1 the
|
||||
// fruit), the amulet, gold, and the KindNone an unrecognized glyph maps
|
||||
// to — report 0, meaning any value is acceptable, which is what C did
|
||||
// with them too.
|
||||
//
|
||||
// Weapons count one past NumWeaponTypes on purpose: Items.Weapons is
|
||||
// sized NumWeaponTypes+1 for the WeaponFlame dragon-breath entry, and
|
||||
// fireBolt really does set Which = WeaponFlame on a live Object. That
|
||||
// entry has no init_dam[] row, so initWeapon and createObj bound
|
||||
// themselves by NumWeaponTypes instead.
|
||||
func whichLimit(kind ObjectKind) int {
|
||||
//nolint:exhaustive // the remaining kinds do not index a per-kind table
|
||||
switch kind {
|
||||
case KindPotion:
|
||||
return int(NumPotionTypes)
|
||||
case KindScroll:
|
||||
return int(NumScrollTypes)
|
||||
case KindRing:
|
||||
return int(NumRingTypes)
|
||||
case KindWand:
|
||||
return int(NumWandTypes)
|
||||
case KindArmor:
|
||||
return int(NumArmorTypes)
|
||||
case KindWeapon:
|
||||
return int(NumWeaponTypes) + 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// PotionKind returns Which as a potion kind; valid only for KindPotion.
|
||||
func (o *Object) PotionKind() PotionKind { return PotionKind(o.Which) }
|
||||
|
||||
// ScrollKind returns Which as a scroll kind; valid only for KindScroll.
|
||||
func (o *Object) ScrollKind() ScrollKind { return ScrollKind(o.Which) }
|
||||
|
||||
// RingKind returns Which as a ring kind; valid only for KindRing.
|
||||
func (o *Object) RingKind() RingKind { return RingKind(o.Which) }
|
||||
|
||||
// WandKind returns Which as a wand kind; valid only for KindWand.
|
||||
func (o *Object) WandKind() WandKind { return WandKind(o.Which) }
|
||||
|
||||
// WeaponKind returns Which as a weapon kind; valid only for KindWeapon.
|
||||
func (o *Object) WeaponKind() WeaponKind { return WeaponKind(o.Which) }
|
||||
|
||||
// ArmorKind returns Which as an armor kind; valid only for KindArmor.
|
||||
func (o *Object) ArmorKind() ArmorKind { return ArmorKind(o.Which) }
|
||||
|
||||
// hasValidWhich reports whether Which is a legal index into this
|
||||
// object's per-kind tables. Objects the game builds itself always
|
||||
// satisfy it; the wizard-create command and a restored save file are the
|
||||
// only two ways a malformed one can appear, and both reject it (see
|
||||
// createObj and validateSnapshotObjects).
|
||||
//
|
||||
// The Which >= 0 arm is unreachable from the keyboard: createObj derives
|
||||
// Which with byte arithmetic (int(ch-'a') + 10), which wraps to a large
|
||||
// positive value rather than going negative. It is kept as
|
||||
// defense-in-depth for the non-keyboard source — a decoded save file,
|
||||
// where Which is an int off the wire and can hold anything — and is
|
||||
// exercised there by TestRestoreRejectsOutOfRangeWhich.
|
||||
func (o *Object) hasValidWhich() bool {
|
||||
limit := whichLimit(o.Kind)
|
||||
|
||||
return limit == 0 || (o.Which >= 0 && o.Which < limit)
|
||||
}
|
||||
|
||||
// wizardCanCreate reports whether Which names something the wizard-create
|
||||
// command can actually build. It is hasValidWhich narrowed for weapons:
|
||||
// WeaponFlame owns a name-table slot but no init_dam[] row, so asking for
|
||||
// it would leave initWeapon nothing to copy.
|
||||
func (o *Object) wizardCanCreate() bool {
|
||||
if o.Kind == KindWeapon {
|
||||
return o.Which >= 0 && o.Which < int(NumWeaponTypes)
|
||||
}
|
||||
|
||||
return o.hasValidWhich()
|
||||
}
|
||||
|
||||
// attachObj is list.c attach(): push item onto the front of a list.
|
||||
func attachObj(list *[]*Object, item *Object) {
|
||||
*list = append([]*Object{item}, *list...)
|
||||
}
|
||||
|
||||
// detachObj is list.c detach(): remove item (by identity) from a list.
|
||||
func detachObj(list *[]*Object, item *Object) {
|
||||
for i, o := range *list {
|
||||
if o == item {
|
||||
*list = append((*list)[:i], (*list)[i+1:]...)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
459
game/options.go
459
game/options.go
@@ -1,459 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "strings"
|
||||
|
||||
// options.c — the option command and ROGUEOPTS parsing.
|
||||
|
||||
// optKind selects the get/put behavior of an option (the C function
|
||||
// pointers o_putfunc/o_getfunc).
|
||||
type optKind int
|
||||
|
||||
const (
|
||||
optBool optKind = iota
|
||||
optSeeFloor
|
||||
optInvT
|
||||
optStr
|
||||
)
|
||||
|
||||
// optDesc describes an option (options.c OPTION).
|
||||
type optDesc struct {
|
||||
name string
|
||||
prompt string
|
||||
kind optKind
|
||||
boolP *bool
|
||||
intP *int
|
||||
strP *string
|
||||
}
|
||||
|
||||
// optList builds the options.c optlist for this game.
|
||||
func (g *RogueGame) optList() []optDesc {
|
||||
o := &g.Options
|
||||
|
||||
return []optDesc{
|
||||
{"terse", "Terse output", optBool, &o.Terse, nil, nil},
|
||||
{"flush", "Flush typeahead during battle", optBool, &o.FightFlush, nil, nil},
|
||||
{"jump", "Show position only at end of run", optBool, &o.Jump, nil, nil},
|
||||
{"seefloor", "Show the lamp-illuminated floor", optSeeFloor, &o.SeeFloor, nil, nil},
|
||||
{"passgo", "Follow turnings in passageways", optBool, &o.PassGo, nil, nil},
|
||||
{"tombstone", "Print out tombstone when killed", optBool, &o.Tombstone, nil, nil},
|
||||
{"inven", "Inventory style", optInvT, nil, &o.InvType, nil},
|
||||
{"name", "Name", optStr, nil, nil, &g.Whoami},
|
||||
{"fruit", "Fruit", optStr, nil, nil, &g.Fruit},
|
||||
{"file", "Save file", optStr, nil, nil, &g.FileName},
|
||||
}
|
||||
}
|
||||
|
||||
// option prints and then sets options from the terminal (options.c option).
|
||||
func (g *RogueGame) option() {
|
||||
hw := g.scr.Hw
|
||||
optlist := g.optList()
|
||||
|
||||
hw.Clear()
|
||||
// Display current values of options
|
||||
for i := range optlist {
|
||||
op := &optlist[i]
|
||||
g.prOptname(op)
|
||||
g.putOpt(op)
|
||||
hw.AddCh('\n')
|
||||
}
|
||||
// Set values
|
||||
hw.Move(0, 0)
|
||||
|
||||
for i := 0; i < len(optlist); i++ {
|
||||
op := &optlist[i]
|
||||
g.prOptname(op)
|
||||
|
||||
retval := g.getOpt(op)
|
||||
if retval != Norm {
|
||||
if retval == Quit {
|
||||
break
|
||||
}
|
||||
// MINUS: back up one option
|
||||
if i > 0 {
|
||||
hw.Move(i-1, 0)
|
||||
i -= 2
|
||||
} else { // trying to back up beyond the top
|
||||
hw.Move(0, 0)
|
||||
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
// Switch back to original screen
|
||||
hw.MvAddStr(NumLines-1, 0, "--Press space to continue--")
|
||||
g.scr.RefreshWin(hw)
|
||||
g.waitFor(' ')
|
||||
g.refresh()
|
||||
g.After = false
|
||||
}
|
||||
|
||||
// prOptname prints out the option name prompt (options.c pr_optname).
|
||||
func (g *RogueGame) prOptname(op *optDesc) {
|
||||
g.scr.Hw.Printwf("%s (\"%s\"): ", op.prompt, op.name)
|
||||
}
|
||||
|
||||
// putOpt prints an option's current value (options.c put_bool/put_str/
|
||||
// put_inv_t).
|
||||
func (g *RogueGame) putOpt(op *optDesc) {
|
||||
hw := g.scr.Hw
|
||||
|
||||
switch op.kind {
|
||||
case optBool, optSeeFloor:
|
||||
hw.AddStr(boolStr(*op.boolP))
|
||||
case optInvT:
|
||||
hw.AddStr(g.data.invTName[*op.intP])
|
||||
case optStr:
|
||||
hw.AddStr(*op.strP)
|
||||
}
|
||||
}
|
||||
|
||||
func boolStr(b bool) string {
|
||||
if b {
|
||||
return "True"
|
||||
}
|
||||
|
||||
return "False"
|
||||
}
|
||||
|
||||
// getOpt reads a new value for an option (options.c get_bool/get_sf/
|
||||
// get_inv_t/get_str dispatch).
|
||||
func (g *RogueGame) getOpt(op *optDesc) int {
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch op.kind {
|
||||
case optBool:
|
||||
return g.getBool(op.boolP)
|
||||
case optSeeFloor:
|
||||
return g.getSf(op.boolP)
|
||||
case optInvT:
|
||||
return g.getInvT(op.intP)
|
||||
default:
|
||||
return g.getStr(op.strP, g.scr.Hw)
|
||||
}
|
||||
}
|
||||
|
||||
// getBool allows changing a boolean option and prints it out (options.c
|
||||
// get_bool).
|
||||
func (g *RogueGame) getBool(bp *bool) int {
|
||||
win := g.scr.Hw
|
||||
oy, ox := win.GetYX()
|
||||
win.AddStr(boolStr(*bp))
|
||||
|
||||
for {
|
||||
win.Move(oy, ox)
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
switch g.readchar() {
|
||||
case 't', 'T':
|
||||
*bp = true
|
||||
case 'f', 'F':
|
||||
*bp = false
|
||||
case '\n', '\r':
|
||||
case Escape:
|
||||
return Quit
|
||||
case '-':
|
||||
return Minus
|
||||
default:
|
||||
win.Move(oy, ox+10)
|
||||
win.AddStr("(T or F)")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
win.Move(oy, ox)
|
||||
win.AddStr(boolStr(*bp))
|
||||
win.AddCh('\n')
|
||||
|
||||
return Norm
|
||||
}
|
||||
|
||||
// getSf changes see_floor and handles the display transition (options.c
|
||||
// get_sf).
|
||||
func (g *RogueGame) getSf(bp *bool) int {
|
||||
wasSf := g.Options.SeeFloor
|
||||
|
||||
retval := g.getBool(bp)
|
||||
if retval == Quit {
|
||||
return Quit
|
||||
}
|
||||
|
||||
if wasSf != g.Options.SeeFloor {
|
||||
if !g.Options.SeeFloor {
|
||||
g.Options.SeeFloor = true
|
||||
g.eraseLamp(g.Player.Pos, g.Player.Room)
|
||||
g.Options.SeeFloor = false
|
||||
} else {
|
||||
g.look(false)
|
||||
}
|
||||
}
|
||||
|
||||
return Norm
|
||||
}
|
||||
|
||||
// getStr sets a string option (options.c get_str). win selects between the
|
||||
// options window and the message line (C's stdscr), which changes the '-'
|
||||
// and mpos behavior.
|
||||
func (g *RogueGame) getStr(opt *string, win *Window) int {
|
||||
onStd := win == g.scr.Std
|
||||
oy, ox := win.GetYX()
|
||||
g.scr.RefreshWin(win)
|
||||
// loop reading in the string, and put it in a temporary buffer
|
||||
var (
|
||||
buf []byte
|
||||
c byte
|
||||
)
|
||||
for {
|
||||
c = g.readchar()
|
||||
if endsInput(c) || (len(buf) == 0 && c == '-' && !onStd) {
|
||||
break
|
||||
}
|
||||
|
||||
buf = g.getStrEdit(win, buf, c, oy, ox)
|
||||
}
|
||||
|
||||
if len(buf) > 0 { // only change option if something has been typed
|
||||
*opt = strucpy(string(buf))
|
||||
}
|
||||
|
||||
win.MvPrintwf(oy, ox, "%s\n", *opt)
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
if onStd {
|
||||
g.Msgs.Mpos += len(buf)
|
||||
}
|
||||
|
||||
return getStrResult(c)
|
||||
}
|
||||
|
||||
// getStrResult maps the terminating key to the C return code (options.c
|
||||
// get_str).
|
||||
func getStrResult(c byte) int {
|
||||
switch c {
|
||||
case '-':
|
||||
return Minus
|
||||
case Escape:
|
||||
return Quit
|
||||
default:
|
||||
return Norm
|
||||
}
|
||||
}
|
||||
|
||||
// endsInput reports the keys that finish line input (options.c get_str).
|
||||
func endsInput(c byte) bool {
|
||||
return c == '\n' || c == '\r' || c == Escape
|
||||
}
|
||||
|
||||
// getStrErase deletes the last character of the buffer (options.c
|
||||
// get_str).
|
||||
func getStrErase(win *Window, buf []byte, oy, ox int) []byte {
|
||||
if len(buf) > 0 {
|
||||
buf = buf[:len(buf)-1]
|
||||
win.Move(oy, ox+len(displayStr(buf)))
|
||||
}
|
||||
|
||||
win.Clrtoeol()
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// getStrEdit applies one key to the line editor's buffer: erase, kill,
|
||||
// home expansion, or a typed character (options.c get_str).
|
||||
func (g *RogueGame) getStrEdit(win *Window, buf []byte, c byte, oy, ox int) []byte {
|
||||
switch {
|
||||
case c == 8 || c == 0x7f:
|
||||
buf = getStrErase(win, buf, oy, ox)
|
||||
case c == CTRL('U'): // kill character
|
||||
buf = buf[:0]
|
||||
|
||||
win.Move(oy, ox)
|
||||
win.Clrtoeol()
|
||||
case len(buf) == 0 && c == '~':
|
||||
buf = append(buf, g.Home...)
|
||||
win.AddStr(g.Home)
|
||||
win.Clrtoeol()
|
||||
case len(buf) >= MaxInp || (!isPrint(c) && c != ' '):
|
||||
return buf // C beeps here
|
||||
default:
|
||||
buf = append(buf, c)
|
||||
win.AddStr(unctrl(c))
|
||||
win.Clrtoeol()
|
||||
}
|
||||
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// displayStr renders a buffer the way the input echo did.
|
||||
func displayStr(buf []byte) string {
|
||||
var sb strings.Builder
|
||||
for _, c := range buf {
|
||||
sb.WriteString(unctrl(c))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// getInvT gets an inventory type name (options.c get_inv_t).
|
||||
func (g *RogueGame) getInvT(ip *int) int {
|
||||
win := g.scr.Hw
|
||||
oy, ox := win.GetYX()
|
||||
win.AddStr(g.data.invTName[*ip])
|
||||
|
||||
for {
|
||||
win.Move(oy, ox)
|
||||
g.scr.RefreshWin(win)
|
||||
|
||||
switch g.readchar() {
|
||||
case 'o', 'O':
|
||||
*ip = InvOver
|
||||
case 's', 'S':
|
||||
*ip = InvSlow
|
||||
case 'c', 'C':
|
||||
*ip = InvClear
|
||||
case '\n', '\r':
|
||||
case Escape:
|
||||
return Quit
|
||||
case '-':
|
||||
return Minus
|
||||
default:
|
||||
win.Move(oy, ox+15)
|
||||
win.AddStr("(O, S, or C)")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
win.MvPrintwf(oy, ox, "%s\n", g.data.invTName[*ip])
|
||||
|
||||
return Norm
|
||||
}
|
||||
|
||||
// ParseOpts parses options from a string, usually taken from the
|
||||
// environment: comma separated values, booleans stated as "name" (true) or
|
||||
// "noname" (false), strings as "name=..." (options.c parse_opts).
|
||||
func (g *RogueGame) ParseOpts(str string) {
|
||||
optlist := g.optList()
|
||||
|
||||
for str != "" {
|
||||
// Get option name
|
||||
i := 0
|
||||
for i < len(str) && isAlpha(str[i]) {
|
||||
i++
|
||||
}
|
||||
|
||||
rest := g.parseOptName(optlist, str[:i], str[i:])
|
||||
// skip to start of next option name
|
||||
for rest != "" && !isAlpha(rest[0]) {
|
||||
rest = rest[1:]
|
||||
}
|
||||
|
||||
str = rest
|
||||
}
|
||||
}
|
||||
|
||||
// parseOptName applies one named option, returning the unconsumed
|
||||
// remainder: "name" turns a boolean on, "noname" turns it off, and
|
||||
// string options consume a value (the option scan of options.c
|
||||
// parse_opts).
|
||||
func (g *RogueGame) parseOptName(optlist []optDesc, name, rest string) string {
|
||||
for oi := range optlist {
|
||||
op := &optlist[oi]
|
||||
|
||||
isBoolOpt := op.kind == optBool || op.kind == optSeeFloor
|
||||
if strings.HasPrefix(op.name, name) && name != "" {
|
||||
if isBoolOpt {
|
||||
*op.boolP = true
|
||||
|
||||
return rest
|
||||
}
|
||||
|
||||
return g.parseOptValue(op, rest)
|
||||
}
|
||||
|
||||
if isBoolOpt && strings.HasPrefix(name, "no") &&
|
||||
strings.HasPrefix(op.name, name[2:]) {
|
||||
*op.boolP = false
|
||||
|
||||
return rest
|
||||
}
|
||||
}
|
||||
|
||||
return rest
|
||||
}
|
||||
|
||||
// parseOptValue consumes an option's "=value" from rest, storing it,
|
||||
// and returns the remainder (the string arm of options.c parse_opts).
|
||||
func (g *RogueGame) parseOptValue(op *optDesc, rest string) string {
|
||||
// Skip to start of string value
|
||||
for rest != "" && rest[0] == '=' {
|
||||
rest = rest[1:]
|
||||
}
|
||||
|
||||
val := rest
|
||||
|
||||
var prefix string
|
||||
if val != "" && val[0] == '~' {
|
||||
prefix = g.Home
|
||||
|
||||
val = val[1:]
|
||||
for val != "" && val[0] == '/' {
|
||||
val = val[1:]
|
||||
}
|
||||
}
|
||||
// Skip to end of string value
|
||||
end := strings.IndexByte(val, ',')
|
||||
if end < 0 {
|
||||
end = len(val)
|
||||
}
|
||||
|
||||
word := val[:end]
|
||||
|
||||
if op.kind == optInvT {
|
||||
g.parseInvType(op, word)
|
||||
} else {
|
||||
*op.strP = prefix + strucpy(word)
|
||||
}
|
||||
|
||||
return val[end:]
|
||||
}
|
||||
|
||||
// parseInvType matches an inventory-style name by prefix (options.c
|
||||
// parse_opts).
|
||||
func (g *RogueGame) parseInvType(op *optDesc, word string) {
|
||||
// check for type of inventory
|
||||
if word != "" {
|
||||
word = string(toUpper(word[0])) + word[1:]
|
||||
}
|
||||
|
||||
for ti, tn := range g.data.invTName {
|
||||
if strings.HasPrefix(tn, word) {
|
||||
*op.intP = ti
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// strucpy copies a string keeping only printable characters, capped at
|
||||
// MAXINP (options.c strucpy).
|
||||
func strucpy(s string) string {
|
||||
if len(s) > MaxInp {
|
||||
s = s[:MaxInp]
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
for i := range len(s) {
|
||||
if isPrint(s[i]) || s[i] == ' ' {
|
||||
sb.WriteByte(s[i])
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
540
game/pack.go
540
game/pack.go
@@ -1,540 +0,0 @@
|
||||
package game
|
||||
|
||||
// pack.c — routines to deal with the pack.
|
||||
|
||||
// addPack picks up an object and adds it to the pack; if obj is non-nil use
|
||||
// it instead of getting it off the ground (pack.c add_pack).
|
||||
func (g *RogueGame) addPack(obj *Object, silent bool) {
|
||||
p := &g.Player
|
||||
fromFloor := false
|
||||
|
||||
if obj == nil {
|
||||
if obj = g.Level.ObjectAt(p.Pos.Y, p.Pos.X); obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fromFloor = true
|
||||
}
|
||||
|
||||
// Check for and deal with scare monster scrolls
|
||||
if g.pickupScareScroll(obj) {
|
||||
return
|
||||
}
|
||||
|
||||
obj, ok := g.packInsert(obj, fromFloor)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
obj.Flags.Set(WasFound)
|
||||
|
||||
// If this was the object of something's desire, that monster will get
|
||||
// mad and run at the hero.
|
||||
for _, op := range g.Level.Monsters {
|
||||
if op.Dest == &obj.Pos {
|
||||
op.Dest = &p.Pos
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Kind == KindAmulet {
|
||||
g.HasAmulet = true
|
||||
}
|
||||
// Notify the user
|
||||
if !silent {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you now have ")
|
||||
}
|
||||
|
||||
g.msg("%s (%c)", g.inventoryName(obj, !g.Options.Terse), obj.PackCh)
|
||||
}
|
||||
}
|
||||
|
||||
// pickupScareScroll crumbles a found scare monster scroll when it is
|
||||
// picked up again; it reports whether it did (pack.c add_pack).
|
||||
func (g *RogueGame) pickupScareScroll(obj *Object) bool {
|
||||
if obj.Kind != KindScroll || obj.ScrollKind() != ScrollScareMonster ||
|
||||
!obj.Flags.Has(WasFound) {
|
||||
return false
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
|
||||
g.Level.RemoveObject(obj)
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
|
||||
|
||||
if p.Room.Flags.Has(Gone) {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
|
||||
}
|
||||
|
||||
g.msg("the scroll turns to dust as you pick it up")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// packInsert places the object in the pack, keeping items of one type
|
||||
// together and merging stackable and grouped items — a translation of
|
||||
// the C linked-list walk in pack.c add_pack. It returns the pack entry
|
||||
// the object ended up as; ok is false when the pack has no room.
|
||||
func (g *RogueGame) packInsert(obj *Object, fromFloor bool) (*Object, bool) {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
p.Pack = append(p.Pack, obj)
|
||||
obj.PackCh = p.nextPackChar()
|
||||
p.Inpack++
|
||||
|
||||
return obj, true
|
||||
}
|
||||
|
||||
merged := false
|
||||
|
||||
// lp is the index to insert after; -1 after a merge means no
|
||||
// insertion.
|
||||
i, lp := packScanKind(p.Pack, obj.Kind)
|
||||
if i < len(p.Pack) {
|
||||
i, lp = packScanWhich(p.Pack, obj, i, lp)
|
||||
|
||||
if op := p.Pack[i]; op.Kind == obj.Kind && op.Which == obj.Which {
|
||||
var ok bool
|
||||
|
||||
obj, lp, merged, ok = g.packMatch(obj, op, i, fromFloor)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !merged && lp != -1 {
|
||||
if !g.packRoom(fromFloor, obj) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
obj.PackCh = p.nextPackChar()
|
||||
p.Pack = append(p.Pack[:lp+1],
|
||||
append([]*Object{obj}, p.Pack[lp+1:]...)...)
|
||||
}
|
||||
|
||||
return obj, true
|
||||
}
|
||||
|
||||
// packScanKind scans to the first pack entry of this kind, returning
|
||||
// its index (or the pack length) and the entry to insert after (the
|
||||
// outer scan of pack.c add_pack).
|
||||
func packScanKind(pack []*Object, kind ObjectKind) (int, int) {
|
||||
lp := -1
|
||||
|
||||
i := 0
|
||||
for ; i < len(pack); i++ {
|
||||
if pack[i].Kind == kind {
|
||||
break
|
||||
}
|
||||
|
||||
lp = i
|
||||
}
|
||||
|
||||
return i, lp
|
||||
}
|
||||
|
||||
// packScanWhich scans within the kind group for the matching subtype
|
||||
// (the inner scan of pack.c add_pack).
|
||||
func packScanWhich(pack []*Object, obj *Object, i, lp int) (int, int) {
|
||||
for pack[i].Kind == obj.Kind && pack[i].Which != obj.Which {
|
||||
lp = i
|
||||
if i+1 >= len(pack) {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return i, lp
|
||||
}
|
||||
|
||||
// packMatch merges the object with a matching pack entry when possible:
|
||||
// stackables merge counts, grouped missiles rejoin their bundle, and
|
||||
// anything else marks the insertion point (the matched-subtype switch
|
||||
// of pack.c add_pack). It returns the resulting entry, the insert-after
|
||||
// index, whether a merge happened, and ok false when the pack is full.
|
||||
func (g *RogueGame) packMatch(
|
||||
obj, op *Object, i int, fromFloor bool,
|
||||
) (*Object, int, bool, bool) {
|
||||
switch {
|
||||
case op.Kind.MergesInPack():
|
||||
if !g.packRoom(fromFloor, obj) {
|
||||
return nil, 0, false, false
|
||||
}
|
||||
|
||||
op.Count++
|
||||
|
||||
return op, -1, true, true
|
||||
case obj.Group != 0:
|
||||
return g.packMatchGroup(obj, i, fromFloor)
|
||||
default:
|
||||
return obj, i, false, true
|
||||
}
|
||||
}
|
||||
|
||||
// packMatchGroup rejoins a grouped missile bundle with its group entry
|
||||
// (the o_group arm of pack.c add_pack).
|
||||
func (g *RogueGame) packMatchGroup(
|
||||
obj *Object, i int, fromFloor bool,
|
||||
) (*Object, int, bool, bool) {
|
||||
p := &g.Player
|
||||
|
||||
lp := i
|
||||
for p.Pack[i].Kind == obj.Kind &&
|
||||
p.Pack[i].Which == obj.Which &&
|
||||
p.Pack[i].Group != obj.Group {
|
||||
lp = i
|
||||
if i+1 >= len(p.Pack) {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
op := p.Pack[i]
|
||||
if op.Kind == obj.Kind && op.Which == obj.Which &&
|
||||
op.Group == obj.Group {
|
||||
op.Count += obj.Count
|
||||
p.Inpack--
|
||||
|
||||
if !g.packRoom(fromFloor, obj) {
|
||||
return nil, 0, false, false
|
||||
}
|
||||
|
||||
return op, -1, true, true
|
||||
}
|
||||
|
||||
return obj, lp, false, true
|
||||
}
|
||||
|
||||
// packRoom sees if there's room in the pack; if not, prints an appropriate
|
||||
// message (pack.c pack_room).
|
||||
func (g *RogueGame) packRoom(fromFloor bool, obj *Object) bool {
|
||||
p := &g.Player
|
||||
if p.Inpack++; p.Inpack > MaxPack {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("there's ")
|
||||
}
|
||||
|
||||
g.addmsgf("no room")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" in your pack")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
|
||||
if fromFloor {
|
||||
g.moveMsg(obj)
|
||||
}
|
||||
|
||||
p.Inpack = MaxPack
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if fromFloor {
|
||||
g.Level.RemoveObject(obj)
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
|
||||
|
||||
if p.Room.Flags.Has(Gone) {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// leavePack takes an item out of the pack (pack.c leave_pack), keeping
|
||||
// the repeat-command bookkeeping; the pack surgery is
|
||||
// Player.removeFromPack.
|
||||
func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object {
|
||||
if obj.Count > 1 && !all {
|
||||
g.LastPick = obj
|
||||
} else {
|
||||
g.LastPick = nil
|
||||
}
|
||||
|
||||
return g.Player.removeFromPack(obj, newobj, all)
|
||||
}
|
||||
|
||||
// inventory lists what is in the pack; returns true if there is something
|
||||
// of the given type (pack.c inventory).
|
||||
func (g *RogueGame) inventory(list []*Object, kind ObjectKind) bool {
|
||||
g.NObjs = 0
|
||||
|
||||
for _, item := range list {
|
||||
if !matchesFilter(kind, item) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.NObjs++
|
||||
g.Msgs.MsgEsc = true
|
||||
|
||||
line := string(item.PackCh) + ") " + g.inventoryName(item, false)
|
||||
if g.addLine("%s", line) == Escape {
|
||||
g.Msgs.MsgEsc = false
|
||||
g.msg("")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
g.Msgs.MsgEsc = false
|
||||
}
|
||||
|
||||
if g.NObjs == 0 {
|
||||
if kind == KindNone {
|
||||
g.msg("%s", g.chooseTerse("empty handed", "you are empty handed"))
|
||||
} else {
|
||||
g.msg("%s", g.chooseTerse("nothing appropriate",
|
||||
"you don't have anything appropriate"))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
g.endLine()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// pickUp adds something to the character's pack (pack.c pick_up).
|
||||
func (g *RogueGame) pickUp(ch byte) {
|
||||
p := &g.Player
|
||||
if p.On(Levitating) {
|
||||
return
|
||||
}
|
||||
|
||||
obj := g.Level.ObjectAt(p.Pos.Y, p.Pos.X)
|
||||
if g.MoveOn {
|
||||
g.moveMsg(obj)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch ch {
|
||||
case Gold:
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
|
||||
g.money(obj.GoldValue)
|
||||
g.Level.RemoveObject(obj)
|
||||
|
||||
p.Room.GoldVal = 0
|
||||
default:
|
||||
g.addPack(nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
// matchesFilter reports whether an item passes a get_item/inventory kind
|
||||
// filter (the C condition in pack.c inventory, untangled): KindNone takes
|
||||
// everything, KindCallable takes anything nameable (not food, not the
|
||||
// amulet), KindRingOrStick takes rings and wands.
|
||||
func matchesFilter(kind ObjectKind, item *Object) bool {
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch kind {
|
||||
case KindNone:
|
||||
return true
|
||||
case KindCallable:
|
||||
return item.Kind != KindFood && item.Kind != KindAmulet
|
||||
case KindRingOrStick:
|
||||
return item.Kind == KindRing || item.Kind == KindWand
|
||||
default:
|
||||
return item.Kind == kind
|
||||
}
|
||||
}
|
||||
|
||||
// moveMsg prints the message if you are just moving onto an object
|
||||
// (pack.c move_msg).
|
||||
func (g *RogueGame) moveMsg(obj *Object) {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you ")
|
||||
}
|
||||
|
||||
g.msg("moved onto %s", g.inventoryName(obj, true))
|
||||
}
|
||||
|
||||
// pickyInven allows the player to inventory a single item (pack.c
|
||||
// picky_inven).
|
||||
func (g *RogueGame) pickyInven() {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you aren't carrying anything")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(p.Pack) == 1 {
|
||||
g.msg("a) %s", g.inventoryName(p.Pack[0], false))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseTerse("item: ", "which item do you wish to inventory: "))
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
mch := g.readchar()
|
||||
if mch == Escape {
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
if mch == obj.PackCh {
|
||||
g.msg("%c) %s", mch, g.inventoryName(obj, false))
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("'%s' not in pack", unctrl(mch))
|
||||
}
|
||||
|
||||
// promptPackItem picks something out of a pack for a purpose (pack.c
|
||||
// get_item); the second result reports whether the player chose an
|
||||
// item.
|
||||
func (g *RogueGame) promptPackItem(purpose string, kind ObjectKind) (*Object, bool) {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you aren't carrying anything")
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if g.Again {
|
||||
return g.repeatLastItem()
|
||||
}
|
||||
|
||||
for {
|
||||
g.promptItemPurpose(purpose)
|
||||
|
||||
ch := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
// Give the poor player a chance to abort the command
|
||||
if ch == Escape {
|
||||
g.resetLast()
|
||||
g.After = false
|
||||
g.msg("")
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
g.NObjs = 1 // normal case: person types one char
|
||||
if ch == '*' {
|
||||
g.Msgs.Mpos = 0
|
||||
if !g.inventory(p.Pack, kind) {
|
||||
g.After = false
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
if obj.PackCh == ch {
|
||||
return obj, true
|
||||
}
|
||||
}
|
||||
|
||||
g.msg("'%s' is not a valid item", unctrl(ch))
|
||||
}
|
||||
}
|
||||
|
||||
// repeatLastItem replays the previous selection for the repeat command
|
||||
// (pack.c get_item).
|
||||
func (g *RogueGame) repeatLastItem() (*Object, bool) {
|
||||
if g.LastPick != nil {
|
||||
return g.LastPick, true
|
||||
}
|
||||
|
||||
g.msg("you ran out")
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// promptItemPurpose prints the "which object do you want to ...?"
|
||||
// prompt (pack.c get_item).
|
||||
func (g *RogueGame) promptItemPurpose(purpose string) {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("which object do you want to ")
|
||||
}
|
||||
|
||||
g.addmsgf("%s", purpose)
|
||||
|
||||
if g.Options.Terse {
|
||||
g.addmsgf(" what")
|
||||
}
|
||||
|
||||
g.msg("? (* for list): ")
|
||||
}
|
||||
|
||||
// money adds or subtracts gold from the pack (pack.c money).
|
||||
func (g *RogueGame) money(value int) {
|
||||
p := &g.Player
|
||||
p.Purse += value
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh())
|
||||
|
||||
if p.Room.Flags.Has(Gone) {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Passage)
|
||||
} else {
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, Floor)
|
||||
}
|
||||
|
||||
if value > 0 {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you found ")
|
||||
}
|
||||
|
||||
g.msg("%d gold pieces", value)
|
||||
}
|
||||
}
|
||||
|
||||
// floorCh returns the appropriate floor character for her room
|
||||
// (pack.c floor_ch).
|
||||
func (g *RogueGame) floorCh() byte {
|
||||
if g.Player.Room.Flags.Has(Gone) {
|
||||
return Passage
|
||||
}
|
||||
|
||||
if g.showFloor() {
|
||||
return Floor
|
||||
}
|
||||
|
||||
return ' '
|
||||
}
|
||||
|
||||
// floorAt returns the character at the hero's position, taking see_floor
|
||||
// into account (pack.c floor_at).
|
||||
func (g *RogueGame) floorAt() byte {
|
||||
ch := g.Level.Char(g.Player.Pos.Y, g.Player.Pos.X)
|
||||
if ch == Floor {
|
||||
ch = g.floorCh()
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// resetLast resets the last command when the current one is aborted
|
||||
// (pack.c reset_last).
|
||||
func (g *RogueGame) resetLast() {
|
||||
g.LastComm = g.LLastComm
|
||||
g.LastDir = g.LLastDir
|
||||
g.LastPick = g.LLastPick
|
||||
}
|
||||
|
||||
// chooseTerse picks the terse or verbose variant of a message.
|
||||
func (g *RogueGame) chooseTerse(terse, verbose string) string {
|
||||
if g.Options.Terse {
|
||||
return terse
|
||||
}
|
||||
|
||||
return verbose
|
||||
}
|
||||
406
game/passages.go
406
game/passages.go
@@ -1,406 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// passages.c — draw the connecting passages.
|
||||
|
||||
// digPassages draws all the passages on a level (passages.c do_passages).
|
||||
func (g *RogueGame) digPassages() {
|
||||
var (
|
||||
isconn [MaxRooms][MaxRooms]bool
|
||||
ingraph [MaxRooms]bool
|
||||
)
|
||||
|
||||
// starting with one room, connect it to a random adjacent room and
|
||||
// then pick a new room to start with.
|
||||
roomcount := 1
|
||||
r1 := g.rnd(MaxRooms)
|
||||
ingraph[r1] = true
|
||||
|
||||
for roomcount < MaxRooms {
|
||||
// find a room to connect with
|
||||
r2 := g.pickNeighbor(r1, func(i int) bool { return !ingraph[i] })
|
||||
if r2 < 0 {
|
||||
// if no adjacent rooms are outside the graph, pick a new
|
||||
// room to look from
|
||||
for {
|
||||
r1 = g.rnd(MaxRooms)
|
||||
if ingraph[r1] {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// otherwise, connect new room to the graph, and draw a tunnel
|
||||
// to it
|
||||
ingraph[r2] = true
|
||||
g.connectRooms(r1, r2)
|
||||
isconn[r1][r2] = true
|
||||
isconn[r2][r1] = true
|
||||
roomcount++
|
||||
}
|
||||
|
||||
// attempt to add passages to the graph a random number of times so that
|
||||
// there isn't always just one unique passage through it.
|
||||
for roomcount = g.rnd(5); roomcount > 0; roomcount-- {
|
||||
r1 = g.rnd(MaxRooms) // a random room to look from
|
||||
// find an adjacent room not already connected; if there is one,
|
||||
// connect it and look for the next added passage
|
||||
r2 := g.pickNeighbor(r1, func(i int) bool { return !isconn[r1][i] })
|
||||
if r2 >= 0 {
|
||||
g.connectRooms(r1, r2)
|
||||
isconn[r1][r2] = true
|
||||
isconn[r2][r1] = true
|
||||
}
|
||||
}
|
||||
|
||||
g.numberPassages()
|
||||
}
|
||||
|
||||
// pickNeighbor reservoir-picks an adjacent room for which ok holds, or
|
||||
// -1 when there is none (passages.c do_passages).
|
||||
func (g *RogueGame) pickNeighbor(r1 int, ok func(int) bool) int {
|
||||
j := 0
|
||||
r2 := -1
|
||||
|
||||
for i := range MaxRooms {
|
||||
if g.data.rdesConn[r1][i] && ok(i) {
|
||||
if j++; g.rnd(j) == 0 {
|
||||
r2 = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r2
|
||||
}
|
||||
|
||||
// corridorPlan is the movement setup connectRooms computes before it
|
||||
// digs (the local variables of passages.c conn).
|
||||
type corridorPlan struct {
|
||||
rpf, rpt *Room // the rooms being joined
|
||||
del Coord // direction of move
|
||||
turnDelta Coord // direction to turn
|
||||
spos, epos Coord // start and end of move
|
||||
distance, turnDistance int // how far to move and to turn
|
||||
}
|
||||
|
||||
// connectRooms draws a corridor from a room in a certain direction
|
||||
// (passages.c conn).
|
||||
func (g *RogueGame) connectRooms(r1, r2 int) {
|
||||
rm, direc := connOrient(r1, r2)
|
||||
|
||||
var plan corridorPlan
|
||||
if direc == 'd' {
|
||||
plan = g.connPlanDown(rm)
|
||||
} else {
|
||||
plan = g.connPlanRight(rm)
|
||||
}
|
||||
|
||||
turnSpot := g.rnd(plan.distance-1) + 1 // where turn starts
|
||||
|
||||
// Draw in the doors on either side of the passage or just put #'s if
|
||||
// the rooms are gone.
|
||||
g.connEnd(plan.rpf, plan.spos)
|
||||
g.connEnd(plan.rpt, plan.epos)
|
||||
|
||||
g.digCorridor(plan, turnSpot)
|
||||
}
|
||||
|
||||
// connOrient picks the upper-left room of the pair and the digging
|
||||
// direction: right for horizontal neighbors, down otherwise (passages.c
|
||||
// conn).
|
||||
func connOrient(r1, r2 int) (int, byte) {
|
||||
rm := min(r1, r2)
|
||||
if abs(r1-r2) == 1 {
|
||||
return rm, 'r'
|
||||
}
|
||||
|
||||
return rm, 'd'
|
||||
}
|
||||
|
||||
// connPlanDown sets up the movement variables for a corridor drawn
|
||||
// downward (passages.c conn).
|
||||
func (g *RogueGame) connPlanDown(rm int) corridorPlan {
|
||||
rpf := &g.Level.Rooms[rm]
|
||||
rpt := &g.Level.Rooms[rm+3] // room pointer of dest
|
||||
|
||||
plan := corridorPlan{
|
||||
rpf: rpf,
|
||||
rpt: rpt,
|
||||
del: Coord{X: 0, Y: 1}, // direction of move
|
||||
spos: rpf.Pos, // start of move
|
||||
epos: rpt.Pos, // end of move
|
||||
}
|
||||
|
||||
if !rpf.Flags.Has(Gone) { // if not gone pick door pos
|
||||
for {
|
||||
plan.spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1
|
||||
|
||||
plan.spos.Y = rpf.Pos.Y + rpf.Max.Y - 1
|
||||
if !rpf.Flags.Has(Maze) ||
|
||||
g.Level.FlagsAt(plan.spos.Y, plan.spos.X).Has(FPassage) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !rpt.Flags.Has(Gone) {
|
||||
for {
|
||||
plan.epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1
|
||||
if !rpt.Flags.Has(Maze) ||
|
||||
g.Level.FlagsAt(plan.epos.Y, plan.epos.X).Has(FPassage) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plan.distance = abs(plan.spos.Y-plan.epos.Y) - 1 // distance to move
|
||||
|
||||
plan.turnDelta.Y = 0 // direction to turn
|
||||
if plan.spos.X < plan.epos.X {
|
||||
plan.turnDelta.X = 1
|
||||
} else {
|
||||
plan.turnDelta.X = -1
|
||||
}
|
||||
|
||||
plan.turnDistance = abs(plan.spos.X - plan.epos.X) // how far to turn
|
||||
|
||||
return plan
|
||||
}
|
||||
|
||||
// connPlanRight sets up the movement variables for a corridor drawn to
|
||||
// the right (passages.c conn).
|
||||
func (g *RogueGame) connPlanRight(rm int) corridorPlan {
|
||||
rpf := &g.Level.Rooms[rm]
|
||||
rpt := &g.Level.Rooms[rm+1]
|
||||
|
||||
plan := corridorPlan{
|
||||
rpf: rpf,
|
||||
rpt: rpt,
|
||||
del: Coord{X: 1, Y: 0},
|
||||
spos: rpf.Pos,
|
||||
epos: rpt.Pos,
|
||||
}
|
||||
|
||||
if !rpf.Flags.Has(Gone) {
|
||||
for {
|
||||
plan.spos.X = rpf.Pos.X + rpf.Max.X - 1
|
||||
|
||||
plan.spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1
|
||||
if !rpf.Flags.Has(Maze) ||
|
||||
g.Level.FlagsAt(plan.spos.Y, plan.spos.X).Has(FPassage) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !rpt.Flags.Has(Gone) {
|
||||
for {
|
||||
plan.epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1
|
||||
if !rpt.Flags.Has(Maze) ||
|
||||
g.Level.FlagsAt(plan.epos.Y, plan.epos.X).Has(FPassage) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plan.distance = abs(plan.spos.X-plan.epos.X) - 1
|
||||
if plan.spos.Y < plan.epos.Y {
|
||||
plan.turnDelta.Y = 1
|
||||
} else {
|
||||
plan.turnDelta.Y = -1
|
||||
}
|
||||
|
||||
plan.turnDelta.X = 0
|
||||
plan.turnDistance = abs(plan.spos.Y - plan.epos.Y)
|
||||
|
||||
return plan
|
||||
}
|
||||
|
||||
// connEnd draws a corridor end: a door on a real room, a passage square
|
||||
// on a gone one (passages.c conn).
|
||||
func (g *RogueGame) connEnd(rp *Room, pos Coord) {
|
||||
if !rp.Flags.Has(Gone) {
|
||||
g.door(rp, pos)
|
||||
} else {
|
||||
g.putPassage(pos)
|
||||
}
|
||||
}
|
||||
|
||||
// digCorridor digs from spos to epos, turning at turnSpot (the digging
|
||||
// loop of passages.c conn).
|
||||
func (g *RogueGame) digCorridor(plan corridorPlan, turnSpot int) {
|
||||
curr := plan.spos
|
||||
distance := plan.distance
|
||||
turnDistance := plan.turnDistance
|
||||
|
||||
for distance > 0 {
|
||||
// Move to new position
|
||||
curr.X += plan.del.X
|
||||
curr.Y += plan.del.Y
|
||||
// Check if we are at the turn place, if so do the turn
|
||||
if distance == turnSpot {
|
||||
for ; turnDistance > 0; turnDistance-- {
|
||||
g.putPassage(curr)
|
||||
curr.X += plan.turnDelta.X
|
||||
curr.Y += plan.turnDelta.Y
|
||||
}
|
||||
}
|
||||
// Continue digging along
|
||||
g.putPassage(curr)
|
||||
|
||||
distance--
|
||||
}
|
||||
|
||||
curr.X += plan.del.X
|
||||
|
||||
curr.Y += plan.del.Y
|
||||
if curr != plan.epos {
|
||||
g.msg("warning, connectivity problem on this level")
|
||||
}
|
||||
}
|
||||
|
||||
// putPassage adds a passage character or secret passage here (passages.c
|
||||
// putpass).
|
||||
func (g *RogueGame) putPassage(cp Coord) {
|
||||
pp := g.Level.At(cp.Y, cp.X)
|
||||
pp.Flags.Set(FPassage)
|
||||
|
||||
if g.rnd(10)+1 < g.Depth && g.rnd(40) == 0 {
|
||||
pp.Flags.Clear(FReal)
|
||||
} else {
|
||||
pp.Ch = Passage
|
||||
}
|
||||
}
|
||||
|
||||
// door adds a door or possibly a secret door; also enters the door in the
|
||||
// exits array of the room (passages.c door).
|
||||
func (g *RogueGame) door(rm *Room, cp Coord) {
|
||||
rm.Exits = append(rm.Exits, cp)
|
||||
|
||||
if rm.Flags.Has(Maze) {
|
||||
return
|
||||
}
|
||||
|
||||
pp := g.Level.At(cp.Y, cp.X)
|
||||
if g.rnd(10)+1 < g.Depth && g.rnd(5) == 0 {
|
||||
if cp.Y == rm.Pos.Y || cp.Y == rm.Pos.Y+rm.Max.Y-1 {
|
||||
pp.Ch = '-'
|
||||
} else {
|
||||
pp.Ch = '|'
|
||||
}
|
||||
|
||||
pp.Flags.Clear(FReal)
|
||||
} else {
|
||||
pp.Ch = Door
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := range NumCols {
|
||||
g.addPassSpot(g.Level.At(y, x), y, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addPassSpot shows one passage or door square for the wizard (the loop
|
||||
// body of passages.c add_pass).
|
||||
func (g *RogueGame) addPassSpot(pp *Place, y, x int) {
|
||||
if !pp.Flags.Has(FPassage) && !hiddenExit(pp.Flags, pp.Ch) {
|
||||
return
|
||||
}
|
||||
|
||||
ch := pp.Ch
|
||||
if pp.Flags.Has(FPassage) {
|
||||
ch = Passage
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen)
|
||||
g.move(y, x)
|
||||
|
||||
switch {
|
||||
case pp.Monst != nil:
|
||||
pp.Monst.OldCh = pp.Ch
|
||||
case pp.Flags.Has(FReal):
|
||||
g.addch(ch)
|
||||
default:
|
||||
g.standout()
|
||||
|
||||
if pp.Flags.Has(FPassage) {
|
||||
g.addch(Passage)
|
||||
} else {
|
||||
g.addch(Door)
|
||||
}
|
||||
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
// hiddenExit reports a door, or a secret door still drawn as a wall
|
||||
// (passages.c add_pass / numpass).
|
||||
func hiddenExit(fp PlaceFlags, ch byte) bool {
|
||||
return ch == Door || (!fp.Has(FReal) && (ch == '|' || ch == '-'))
|
||||
}
|
||||
|
||||
// numberPassages assigns a number to each passageway (passages.c passnum).
|
||||
func (g *RogueGame) numberPassages() {
|
||||
g.pnum = 0
|
||||
|
||||
g.newpnum = false
|
||||
for i := range g.Level.Passages {
|
||||
g.Level.Passages[i].Exits = g.Level.Passages[i].Exits[:0]
|
||||
}
|
||||
|
||||
for i := range g.Level.Rooms {
|
||||
rp := &g.Level.Rooms[i]
|
||||
for j := range rp.Exits {
|
||||
g.newpnum = true
|
||||
g.numberPassage(rp.Exits[j].Y, rp.Exits[j].X)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// numberPassage numbers a passageway square and its brethren (passages.c
|
||||
// numpass).
|
||||
func (g *RogueGame) numberPassage(y, x int) {
|
||||
if x >= NumCols || x < 0 || y >= NumLines || y <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fp := g.Level.FlagsAt(y, x)
|
||||
if fp.Has(FPassNum) {
|
||||
return
|
||||
}
|
||||
|
||||
if g.newpnum {
|
||||
g.pnum++
|
||||
g.newpnum = false
|
||||
}
|
||||
// check to see if it is a door or secret door, i.e., a new exit, or a
|
||||
// numerable type of place
|
||||
if hiddenExit(*fp, g.Level.Char(y, x)) {
|
||||
rp := &g.Level.Passages[g.pnum]
|
||||
rp.Exits = append(rp.Exits, Coord{Y: y, X: x})
|
||||
} else if !fp.Has(FPassage) {
|
||||
return
|
||||
}
|
||||
|
||||
*fp |= PlaceFlags(g.pnum) //nolint:gosec // G115: pnum < MaxPass=13
|
||||
// recurse on the surrounding places
|
||||
g.numberPassage(y+1, x)
|
||||
g.numberPassage(y-1, x)
|
||||
g.numberPassage(y, x+1)
|
||||
g.numberPassage(y, x-1)
|
||||
}
|
||||
|
||||
// abs is C abs() for ints.
|
||||
func abs(n int) int {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
368
game/potions.go
368
game/potions.go
@@ -1,368 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// potions.c — functions for dealing with potions.
|
||||
|
||||
// pact describes a standard fuse-based potion (potions.c PACT).
|
||||
type pact struct {
|
||||
flags CreatureFlags
|
||||
daemon DaemonID
|
||||
time int
|
||||
high string // message when hallucinating
|
||||
straight string
|
||||
}
|
||||
|
||||
// quaff drinks a potion from the pack (potions.c quaff).
|
||||
func (g *RogueGame) quaff() {
|
||||
p := &g.Player
|
||||
obj, ok := g.promptPackItem("quaff", KindPotion)
|
||||
// Make certain that it is something that we want to drink
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindPotion {
|
||||
if !g.Options.Terse {
|
||||
g.msg("yuk! Why would you want to drink that?")
|
||||
} else {
|
||||
g.msg("that's undrinkable")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj == p.CurWeapon {
|
||||
p.CurWeapon = nil
|
||||
}
|
||||
|
||||
// Calculate the effect it has on the poor guy.
|
||||
trip := p.On(Hallucinating)
|
||||
|
||||
g.leavePack(obj, false, false)
|
||||
|
||||
if h := g.data.quaffHandler(obj); h != nil {
|
||||
h(g, trip)
|
||||
}
|
||||
|
||||
g.status()
|
||||
// Throw the item away. A malformed potion has no lore entry to name,
|
||||
// so it is drunk for no effect and never prompts to be called anything.
|
||||
if obj.hasValidWhich() {
|
||||
g.callIt(&g.Items.Potions[obj.Which])
|
||||
}
|
||||
}
|
||||
|
||||
// The per-potion effect handlers, dispatched through
|
||||
// gameData.quaffHandlers. Each is one case of the C quaff switch.
|
||||
|
||||
func (g *RogueGame) quaffConfusion(trip bool) {
|
||||
g.applyPotionFuse(PotionConfusion, !trip)
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffPoison(bool) {
|
||||
g.Items.Potions[PotionPoison].Know = true
|
||||
if g.Player.IsWearing(RingSustainStrength) {
|
||||
g.msg("you feel momentarily sick")
|
||||
} else {
|
||||
g.changeStrength(-(g.rnd(3) + 1))
|
||||
g.msg("you feel very sick now")
|
||||
g.comeDown(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffHealing(bool) {
|
||||
p := &g.Player
|
||||
|
||||
g.Items.Potions[PotionHealing].Know = true
|
||||
if p.Stats.HP += g.roll(p.Stats.Lvl, 4); p.Stats.HP > p.Stats.MaxHP {
|
||||
p.Stats.MaxHP++
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
g.msg("you begin to feel better")
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffGainStrength(bool) {
|
||||
g.Items.Potions[PotionGainStrength].Know = true
|
||||
g.changeStrength(1)
|
||||
g.msg("you feel stronger, now. What bulging muscles!")
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffDetectMonsters(bool) {
|
||||
g.Player.Flags.Set(SenseMonsters)
|
||||
g.Fuse(DTurnSee, 1, HuhDuration, After)
|
||||
|
||||
if !g.turnSee(false) {
|
||||
g.msg("you have a %s feeling for a moment, then it passes",
|
||||
g.chooseStr("normal", "strange"))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffDetectMagic(bool) {
|
||||
// Potion of magic detection. Show the potions and scrolls
|
||||
show := false
|
||||
|
||||
if len(g.Level.Objects) > 0 {
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
for _, tp := range g.Level.Objects {
|
||||
if g.isMagic(tp) {
|
||||
show = true
|
||||
|
||||
g.scr.Hw.MvAddCh(tp.Pos.Y, tp.Pos.X, Magic)
|
||||
g.Items.Potions[PotionDetectMagic].Know = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
for _, tp := range mp.Pack {
|
||||
if g.isMagic(tp) {
|
||||
show = true
|
||||
|
||||
g.scr.Hw.MvAddCh(mp.Pos.Y, mp.Pos.X, Magic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if show {
|
||||
g.Items.Potions[PotionDetectMagic].Know = true
|
||||
g.showWin("You sense the presence of magic on this level.--More--")
|
||||
} else {
|
||||
g.msg("you have a %s feeling for a moment, then it passes",
|
||||
g.chooseStr("normal", "strange"))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffLSD(trip bool) {
|
||||
p := &g.Player
|
||||
if !trip {
|
||||
if p.On(SenseMonsters) {
|
||||
g.turnSee(false)
|
||||
}
|
||||
|
||||
g.StartDaemon(DVisuals, 0, Before)
|
||||
g.SeenStairs = g.seenStairs()
|
||||
}
|
||||
|
||||
g.applyPotionFuse(PotionLSD, true)
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffSeeInvisible(bool) {
|
||||
show := g.Player.On(CanSeeInvisible)
|
||||
|
||||
g.applyPotionFuse(PotionSeeInvisible, false)
|
||||
|
||||
if !show {
|
||||
g.invisOn()
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffRaiseLevel(bool) {
|
||||
g.Items.Potions[PotionRaiseLevel].Know = true
|
||||
g.msg("you suddenly feel much more skillful")
|
||||
g.raiseLevel()
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffExtraHealing(bool) {
|
||||
p := &g.Player
|
||||
|
||||
g.Items.Potions[PotionExtraHealing].Know = true
|
||||
if p.Stats.HP += g.roll(p.Stats.Lvl, 8); p.Stats.HP > p.Stats.MaxHP {
|
||||
if p.Stats.HP > p.Stats.MaxHP+p.Stats.Lvl+1 {
|
||||
p.Stats.MaxHP++
|
||||
}
|
||||
|
||||
p.Stats.MaxHP++
|
||||
p.Stats.HP = p.Stats.MaxHP
|
||||
}
|
||||
|
||||
g.sight(0)
|
||||
g.comeDown(0)
|
||||
g.msg("you begin to feel much better")
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffHaste(bool) {
|
||||
g.Items.Potions[PotionHaste].Know = true
|
||||
|
||||
g.After = false
|
||||
if g.addHaste(true) {
|
||||
g.msg("you feel yourself moving much faster")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffRestoreStrength(bool) {
|
||||
p := &g.Player
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, -p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, -p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
if p.Stats.Str < p.MaxStats.Str {
|
||||
p.Stats.Str = p.MaxStats.Str
|
||||
}
|
||||
|
||||
if p.IsRing(Left, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, p.CurRing[Left].Bonus)
|
||||
}
|
||||
|
||||
if p.IsRing(Right, RingAddStrength) {
|
||||
addStr(&p.Stats.Str, p.CurRing[Right].Bonus)
|
||||
}
|
||||
|
||||
g.msg("hey, this tastes great. It make you feel warm all over")
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffBlindness(bool) {
|
||||
g.applyPotionFuse(PotionBlindness, true)
|
||||
}
|
||||
|
||||
func (g *RogueGame) quaffLevitation(bool) {
|
||||
g.applyPotionFuse(PotionLevitation, true)
|
||||
}
|
||||
|
||||
// raiseLevel: the guy just magically went up a level (potions.c
|
||||
// raise_level).
|
||||
func (g *RogueGame) raiseLevel() {
|
||||
g.Player.Stats.Exp = g.data.eLevels[g.Player.Stats.Lvl-1] + 1
|
||||
g.checkLevel()
|
||||
}
|
||||
|
||||
// applyPotionFuse does a potion with standard setup: it uses a fuse and
|
||||
// turns on a flag (potions.c do_pot).
|
||||
func (g *RogueGame) applyPotionFuse(kind PotionKind, knowit bool) {
|
||||
pp := &g.data.pActions[kind]
|
||||
if !g.Items.Potions[kind].Know {
|
||||
g.Items.Potions[kind].Know = knowit
|
||||
}
|
||||
|
||||
t := g.spread(pp.time)
|
||||
if !g.Player.On(pp.flags) {
|
||||
g.Player.Flags.Set(pp.flags)
|
||||
g.Fuse(pp.daemon, 0, t, After)
|
||||
g.look(false)
|
||||
} else {
|
||||
g.Lengthen(pp.daemon, t)
|
||||
}
|
||||
|
||||
high, straight := pp.high, pp.straight
|
||||
|
||||
if kind == PotionSeeInvisible {
|
||||
s := fmt.Sprintf("this potion tastes like %s juice", g.Fruit)
|
||||
high, straight = s, s
|
||||
}
|
||||
|
||||
g.msg("%s", g.chooseStr(high, straight))
|
||||
}
|
||||
|
||||
// isMagic reports whether an object radiates magic (potions.c is_magic).
|
||||
func (g *RogueGame) isMagic(o *Object) bool {
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch o.Kind {
|
||||
case KindArmor:
|
||||
return o.Flags.Has(Protected) || o.ArmorClass != g.data.armorClass(o.Which)
|
||||
case KindWeapon:
|
||||
return o.HPlus != 0 || o.DPlus != 0
|
||||
case KindPotion, KindScroll, KindWand, KindRing, KindAmulet:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// invisOn turns on the ability to see invisible monsters (potions.c
|
||||
// invis_on).
|
||||
func (g *RogueGame) invisOn() {
|
||||
g.Player.Flags.Set(CanSeeInvisible)
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
if mp.On(Invisible) && g.seeMonst(mp) && !g.Player.On(Hallucinating) {
|
||||
g.mvaddch(mp.Pos.Y, mp.Pos.X, mp.Disguise)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// turnSee puts on or off seeing monsters on this level (potions.c
|
||||
// turn_see).
|
||||
func (g *RogueGame) turnSee(turnOff bool) bool {
|
||||
addNew := false
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
g.move(mp.Pos.Y, mp.Pos.X)
|
||||
|
||||
canSee := g.seeMonst(mp)
|
||||
if turnOff {
|
||||
if !canSee {
|
||||
g.addch(mp.OldCh)
|
||||
}
|
||||
} else if g.showSensed(mp, canSee) {
|
||||
addNew = true
|
||||
}
|
||||
}
|
||||
|
||||
if turnOff {
|
||||
g.Player.Flags.Clear(SenseMonsters)
|
||||
} else {
|
||||
g.Player.Flags.Set(SenseMonsters)
|
||||
}
|
||||
|
||||
return addNew
|
||||
}
|
||||
|
||||
// showSensed draws one monster for monster sense, standout when it is
|
||||
// otherwise invisible; it reports whether the monster was newly revealed
|
||||
// (the turn-on arm of the C turn_see loop).
|
||||
func (g *RogueGame) showSensed(mp *Monster, canSee bool) bool {
|
||||
if !canSee {
|
||||
g.standout()
|
||||
}
|
||||
|
||||
if !g.Player.On(Hallucinating) {
|
||||
g.addch(mp.Type)
|
||||
} else {
|
||||
g.addch(g.randomMonsterLetter())
|
||||
}
|
||||
|
||||
if !canSee {
|
||||
g.standend()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// seenStairs reports whether the player has seen the stairs (potions.c
|
||||
// seen_stairs).
|
||||
func (g *RogueGame) seenStairs() bool {
|
||||
st := g.Level.Stairs
|
||||
g.move(st.Y, st.X)
|
||||
|
||||
if g.inch() == Stairs { // it's on the map
|
||||
return true
|
||||
}
|
||||
|
||||
if g.Player.Pos == st { // it's under him
|
||||
return true
|
||||
}
|
||||
// if a monster is on the stairs, this gets hairy
|
||||
if tp := g.Level.MonsterAt(st.Y, st.X); tp != nil {
|
||||
if g.seeMonst(tp) && tp.On(Awake) { // visible and awake:
|
||||
return true // it must have moved there
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) && tp.OldCh == Stairs {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
182
game/rings.go
182
game/rings.go
@@ -1,182 +0,0 @@
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// rings.c — routines dealing specifically with rings.
|
||||
|
||||
// ringOn puts a ring on a hand (rings.c ring_on).
|
||||
func (g *RogueGame) ringOn() {
|
||||
p := &g.Player
|
||||
obj, ok := g.promptPackItem("put on", KindRing)
|
||||
// Make certain that it is something that we want to wear
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindRing {
|
||||
g.msg("%s", g.chooseTerse("not a ring",
|
||||
"it would be difficult to wrap that around a finger"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// find out which hand to put it on
|
||||
if g.isCurrent(obj) {
|
||||
return
|
||||
}
|
||||
|
||||
ring := g.pickRingHand()
|
||||
if ring < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
p.CurRing[ring] = obj
|
||||
|
||||
// Calculate the effect it has on the poor guy.
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.RingKind() {
|
||||
case RingAddStrength:
|
||||
g.changeStrength(obj.Bonus)
|
||||
case RingSeeInvisible:
|
||||
g.invisOn()
|
||||
case RingAggravateMonsters:
|
||||
g.aggravate()
|
||||
}
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you are now wearing ")
|
||||
}
|
||||
|
||||
g.msg("%s (%c)", g.inventoryName(obj, true), obj.PackCh)
|
||||
}
|
||||
|
||||
// pickRingHand chooses the hand for a new ring, asking when both are
|
||||
// free; negative aborts (rings.c ring_on).
|
||||
func (g *RogueGame) pickRingHand() int {
|
||||
p := &g.Player
|
||||
|
||||
switch {
|
||||
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
|
||||
return g.gethand()
|
||||
case p.CurRing[Left] == nil:
|
||||
return Left
|
||||
case p.CurRing[Right] == nil:
|
||||
return Right
|
||||
default:
|
||||
g.msg("%s", g.chooseTerse("wearing two",
|
||||
"you already have a ring on each hand"))
|
||||
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// ringOff takes off a ring (rings.c ring_off).
|
||||
func (g *RogueGame) ringOff() {
|
||||
p := &g.Player
|
||||
|
||||
var ring int
|
||||
|
||||
switch {
|
||||
case p.CurRing[Left] == nil && p.CurRing[Right] == nil:
|
||||
if g.Options.Terse {
|
||||
g.msg("no rings")
|
||||
} else {
|
||||
g.msg("you aren't wearing any rings")
|
||||
}
|
||||
|
||||
return
|
||||
case p.CurRing[Left] == nil:
|
||||
ring = Right
|
||||
case p.CurRing[Right] == nil:
|
||||
ring = Left
|
||||
default:
|
||||
if ring = g.gethand(); ring < 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
obj := p.CurRing[ring]
|
||||
if obj == nil {
|
||||
g.msg("not wearing such a ring")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if g.dropCheck(obj) {
|
||||
g.msg("was wearing %s(%c)", g.inventoryName(obj, true), obj.PackCh)
|
||||
}
|
||||
}
|
||||
|
||||
// gethand asks which hand the hero is interested in (rings.c gethand).
|
||||
func (g *RogueGame) gethand() int {
|
||||
for {
|
||||
if g.Options.Terse {
|
||||
g.msg("left or right ring? ")
|
||||
} else {
|
||||
g.msg("left hand or right hand? ")
|
||||
}
|
||||
|
||||
c := g.readchar()
|
||||
if c == Escape {
|
||||
return -1
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if c == 'l' || c == 'L' {
|
||||
return Left
|
||||
}
|
||||
|
||||
if c == 'r' || c == 'R' {
|
||||
return Right
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("L or R")
|
||||
} else {
|
||||
g.msg("please type L or R")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ringEat reports how much food the ring on the given hand uses up
|
||||
// (rings.c ring_eat).
|
||||
func (g *RogueGame) ringEat(hand int) int {
|
||||
ring := g.Player.CurRing[hand]
|
||||
if ring == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
eat := g.data.ringUses[ring.RingKind()]
|
||||
if eat < 0 {
|
||||
if g.rnd(-eat) == 0 {
|
||||
eat = 1
|
||||
} else {
|
||||
eat = 0
|
||||
}
|
||||
}
|
||||
|
||||
if ring.RingKind() == RingSlowDigestion {
|
||||
eat = -eat
|
||||
}
|
||||
|
||||
return eat
|
||||
}
|
||||
|
||||
// ringNum prints ring bonuses (rings.c ring_num). The unused game
|
||||
// parameter keeps the nameit prfunc signature.
|
||||
func ringNum(_ *RogueGame, obj *Object) string {
|
||||
if !obj.Flags.Has(Known) {
|
||||
return ""
|
||||
}
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.RingKind() {
|
||||
case RingProtection, RingAddStrength, RingIncreaseDamage, RingDexterity:
|
||||
return fmt.Sprintf(" [%s]", num(obj.Bonus, 0, Ring))
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
290
game/rip.go
290
game/rip.go
@@ -1,290 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// rip.c — the fun ends: death or a total win.
|
||||
//
|
||||
// The C functions here call exit(). One game run is one process, so the
|
||||
// port does the same: myExit restores the terminal and exits directly.
|
||||
|
||||
// myExit leaves the process properly (main.c my_exit): it restores the
|
||||
// terminal and ends the process. Every C caller exited with status 0.
|
||||
func (g *RogueGame) myExit() {
|
||||
g.scr.Fini()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// death does something really fun when he dies (rip.c death).
|
||||
func (g *RogueGame) death(monst byte) {
|
||||
p := &g.Player
|
||||
p.Purse -= p.Purse / 10
|
||||
|
||||
g.clear()
|
||||
|
||||
killer := g.killname(monst, false)
|
||||
if !g.Options.Tombstone {
|
||||
g.scr.Std.MvPrintwf(NumLines-2, 0, "Killed by ")
|
||||
|
||||
if monst != 's' && monst != 'h' {
|
||||
g.printw("a%s ", vowelstr(killer))
|
||||
}
|
||||
|
||||
g.printw("%s with %d gold", killer, p.Purse)
|
||||
} else {
|
||||
year := time.Now().Year()
|
||||
|
||||
for i, line := range g.data.ripArt {
|
||||
g.scr.Std.MvAddStr(8+i, 0, line)
|
||||
}
|
||||
|
||||
g.mvaddstr(17, center(killer), killer)
|
||||
|
||||
if monst == 's' || monst == 'h' {
|
||||
g.mvaddch(16, 32, ' ')
|
||||
} else {
|
||||
g.mvaddstr(16, 33, vowelstr(killer))
|
||||
}
|
||||
|
||||
g.mvaddstr(14, center(g.Whoami), g.Whoami)
|
||||
|
||||
au := fmt.Sprintf("%d Au", p.Purse)
|
||||
g.mvaddstr(15, center(au), au)
|
||||
g.mvaddstr(18, 26, fmt.Sprintf("%4d", year))
|
||||
}
|
||||
|
||||
g.mvaddstr(NumLines-1, 0, "[Press return to continue]")
|
||||
g.refresh()
|
||||
|
||||
flags := 0
|
||||
if g.HasAmulet {
|
||||
flags = 3
|
||||
}
|
||||
|
||||
g.score(p.Purse, flags, monst)
|
||||
g.waitFor('\n')
|
||||
g.myExit()
|
||||
}
|
||||
|
||||
// center returns the column to center the given string on the tombstone
|
||||
// (rip.c center).
|
||||
func center(str string) int {
|
||||
return 28 - (len(str)+1)/2
|
||||
}
|
||||
|
||||
// totalWinner is the code for a winner (rip.c total_winner).
|
||||
func (g *RogueGame) totalWinner() {
|
||||
p := &g.Player
|
||||
g.clear()
|
||||
g.standout()
|
||||
|
||||
banner := []string{
|
||||
" ",
|
||||
" @ @ @ @ @ @@@ @ @ ",
|
||||
" @ @ @@ @@ @ @ @ @ ",
|
||||
" @ @ @@@ @ @ @ @ @ @@@ @@@@ @@@ @ @@@ @ ",
|
||||
" @@@@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ ",
|
||||
" @ @ @ @ @ @ @ @@@@ @ @ @@@@@ @ @ @ ",
|
||||
" @ @ @ @ @ @@ @ @ @ @ @ @ @ @ @ @ ",
|
||||
" @@@ @@@ @@ @ @ @ @@@@ @@@@ @@@ @@@ @@ @ ",
|
||||
" ",
|
||||
" Congratulations, you have made it to the light of day! ",
|
||||
}
|
||||
for i, line := range banner {
|
||||
g.scr.Std.MvAddStr(i, 0, line)
|
||||
}
|
||||
|
||||
g.standend()
|
||||
g.scr.Std.MvAddStr(10, 0,
|
||||
"You have joined the elite ranks of those who have escaped the")
|
||||
g.scr.Std.MvAddStr(11, 0,
|
||||
"Dungeons of Doom alive. You journey home and sell all your loot at")
|
||||
g.scr.Std.MvAddStr(12, 0, "a great profit and are admitted to the Fighters' Guild.")
|
||||
g.mvaddstr(NumLines-1, 0, "--Press space to continue--")
|
||||
g.refresh()
|
||||
g.waitFor(' ')
|
||||
g.clear()
|
||||
g.mvaddstr(0, 0, " Worth Item")
|
||||
g.move(1, 0)
|
||||
|
||||
oldpurse := p.Purse
|
||||
line := 1
|
||||
|
||||
for _, obj := range p.Pack {
|
||||
worth := g.objectWorth(obj)
|
||||
|
||||
g.scr.Std.MvPrintwf(line, 0, "%c) %5d %s", obj.PackCh, worth,
|
||||
g.inventoryName(obj, false))
|
||||
line++
|
||||
p.Purse += worth
|
||||
}
|
||||
|
||||
g.scr.Std.MvPrintwf(line, 0, " %5d Gold Pieces ", oldpurse)
|
||||
g.refresh()
|
||||
g.score(p.Purse, 2, ' ')
|
||||
g.myExit()
|
||||
}
|
||||
|
||||
// objectWorth appraises one pack item on the way out, marking it known
|
||||
// (the switch of rip.c total_winner).
|
||||
func (g *RogueGame) objectWorth(obj *Object) int {
|
||||
// Same defense as inventoryName: most arms below appraise from a
|
||||
// per-kind table at obj.Which, so a malformed item is worth nothing
|
||||
// rather than a panic on the way to the scoreboard.
|
||||
if !obj.hasValidWhich() {
|
||||
return 0
|
||||
}
|
||||
|
||||
it := &g.Items
|
||||
worth := 0
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.Kind {
|
||||
case KindFood:
|
||||
worth = 2 * obj.Count
|
||||
case KindWeapon:
|
||||
worth = it.Weapons[obj.Which].Worth
|
||||
worth *= 3*(obj.HPlus+obj.DPlus) + obj.Count
|
||||
obj.Flags.Set(Known)
|
||||
case KindArmor:
|
||||
worth = it.Armors[obj.Which].Worth
|
||||
worth += (9 - obj.ArmorClass) * 100
|
||||
worth += 10 * (g.data.armorClass(obj.Which) - obj.ArmorClass)
|
||||
obj.Flags.Set(Known)
|
||||
case KindScroll:
|
||||
worth = loreWorth(&it.Scrolls[obj.Which], obj.Count)
|
||||
case KindPotion:
|
||||
worth = loreWorth(&it.Potions[obj.Which], obj.Count)
|
||||
case KindRing:
|
||||
worth = g.ringWorth(obj)
|
||||
case KindWand:
|
||||
worth = g.wandWorth(obj)
|
||||
case KindAmulet:
|
||||
worth = 1000
|
||||
}
|
||||
|
||||
if worth < 0 {
|
||||
worth = 0
|
||||
}
|
||||
|
||||
return worth
|
||||
}
|
||||
|
||||
// loreWorth appraises a scroll or potion, halved when unidentified, and
|
||||
// identifies it (rip.c total_winner).
|
||||
func loreWorth(op *ObjInfo, count int) int {
|
||||
worth := op.Worth * count
|
||||
if !op.Know {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
op.Know = true
|
||||
|
||||
return worth
|
||||
}
|
||||
|
||||
// ringWorth appraises a ring: bonus rings gain by their bonus, cursed
|
||||
// ones are junk (rip.c total_winner).
|
||||
func (g *RogueGame) ringWorth(obj *Object) int {
|
||||
op := &g.Items.Rings[obj.Which]
|
||||
worth := op.Worth
|
||||
|
||||
if obj.RingKind() == RingAddStrength || obj.RingKind() == RingIncreaseDamage ||
|
||||
obj.RingKind() == RingProtection || obj.RingKind() == RingDexterity {
|
||||
if obj.Bonus > 0 {
|
||||
worth += obj.Bonus * 100
|
||||
} else {
|
||||
worth = 10
|
||||
}
|
||||
}
|
||||
|
||||
if !obj.Flags.Has(Known) {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
obj.Flags.Set(Known)
|
||||
|
||||
op.Know = true
|
||||
|
||||
return worth
|
||||
}
|
||||
|
||||
// wandWorth appraises a wand or staff by its charges (rip.c
|
||||
// total_winner).
|
||||
func (g *RogueGame) wandWorth(obj *Object) int {
|
||||
op := &g.Items.Sticks[obj.Which]
|
||||
worth := op.Worth
|
||||
|
||||
worth += 20 * obj.Charges
|
||||
if !obj.Flags.Has(Known) {
|
||||
worth /= 2
|
||||
}
|
||||
|
||||
obj.Flags.Set(Known)
|
||||
|
||||
op.Know = true
|
||||
|
||||
return worth
|
||||
}
|
||||
|
||||
// killname converts a code to a monster name (rip.c killname).
|
||||
func (g *RogueGame) killname(monst byte, doart bool) string {
|
||||
var (
|
||||
sp string
|
||||
article bool
|
||||
)
|
||||
|
||||
if isUpper(monst) {
|
||||
sp = g.Monsters[monst-'A'].Name
|
||||
article = true
|
||||
} else {
|
||||
sp = "Wally the Wonder Badger"
|
||||
article = false
|
||||
|
||||
for _, hp := range g.data.killnameTable {
|
||||
if hp.Ch == monst {
|
||||
sp = hp.Desc
|
||||
article = hp.Print
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if doart && article {
|
||||
return "a" + vowelstr(sp) + " " + sp
|
||||
}
|
||||
|
||||
return sp
|
||||
}
|
||||
|
||||
// DeathDemo implements the -d command line option (main.c): burn some
|
||||
// random numbers to break patterns, then die a random death. It does not
|
||||
// return — death exits the process.
|
||||
func (g *RogueGame) DeathDemo() {
|
||||
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
|
||||
// death_monst).
|
||||
func (g *RogueGame) deathMonst() byte {
|
||||
poss := []byte{
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
||||
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||
'Y', 'Z', 'a', 'b', 'h', 'd', 's',
|
||||
' ', // generates the "Wally the Wonder Badger" message
|
||||
}
|
||||
|
||||
return poss[g.rnd(len(poss))]
|
||||
}
|
||||
56
game/rng.go
56
game/rng.go
@@ -1,56 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// Rng is the original Rogue linear congruential generator. The C RN macro is
|
||||
//
|
||||
// #define RN (((seed = seed*11109+13849) >> 16) & 0xffff)
|
||||
//
|
||||
// with `seed` a C int; Seed is int32 so the multiplication wraps exactly the
|
||||
// way 32-bit C arithmetic does. A given seed therefore produces the same
|
||||
// dungeon as the C game.
|
||||
type Rng struct {
|
||||
Seed int32
|
||||
}
|
||||
|
||||
// Rnd picks a very random number in [0, rng) (main.c rnd).
|
||||
func (r *Rng) Rnd(rng int) int {
|
||||
if rng == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
v := r.next()
|
||||
if v < 0 {
|
||||
v = -v
|
||||
}
|
||||
|
||||
return v % rng
|
||||
}
|
||||
|
||||
// Roll rolls a number of dice (main.c roll).
|
||||
func (r *Rng) Roll(number, sides int) int {
|
||||
dtotal := 0
|
||||
for ; number > 0; number-- {
|
||||
dtotal += r.Rnd(sides) + 1
|
||||
}
|
||||
|
||||
return dtotal
|
||||
}
|
||||
|
||||
// next steps the generator and returns the next raw value (the RN macro).
|
||||
func (r *Rng) next() int {
|
||||
r.Seed = r.Seed*11109 + 13849
|
||||
|
||||
return int(r.Seed>>16) & 0xffff
|
||||
}
|
||||
|
||||
// rnd is the ported code's spelling of C rnd(): every call site in the C
|
||||
// sources reads rnd(x), and keeping that shape makes cross-checking easy.
|
||||
func (g *RogueGame) rnd(rng int) int { return g.Rng.Rnd(rng) }
|
||||
|
||||
// roll is C roll().
|
||||
func (g *RogueGame) roll(number, sides int) int { return g.Rng.Roll(number, sides) }
|
||||
|
||||
// spread gives a fuzzy number centered on nm (misc.c spread).
|
||||
func (g *RogueGame) spread(nm int) int {
|
||||
return nm - nm/20 + g.rnd(nm/10)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// The golden values below were produced by the original C generator
|
||||
// (seed = seed*11109+13849; rnd(range) = abs(RN) % range) compiled and run
|
||||
// on this machine. They lock in seed compatibility with the C game.
|
||||
func TestRndMatchesCImplementation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
golden := map[int32][]int{
|
||||
1: {0, 30, 79, 7, 87, 1, 23, 7, 57, 98},
|
||||
12345: {92, 92, 45, 98, 24, 39, 92, 67, 3, 7},
|
||||
2026: {43, 90, 6, 2, 43, 34, 20, 5, 67, 71},
|
||||
1751808000: {61, 51, 56, 99, 68, 45, 88, 50, 46, 88},
|
||||
}
|
||||
for seed, want := range golden {
|
||||
r := &Rng{Seed: seed}
|
||||
for i, w := range want {
|
||||
if got := r.Rnd(100); got != w {
|
||||
t.Errorf("seed %d: rnd(100) call %d = %d, want %d", seed, i, got, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRollMatchesCImplementation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
golden := map[int32][]int{
|
||||
1: {6, 8, 10},
|
||||
12345: {10, 10, 15},
|
||||
2026: {10, 10, 13},
|
||||
1751808000: {7, 9, 9},
|
||||
}
|
||||
for seed, want := range golden {
|
||||
r := &Rng{Seed: seed}
|
||||
for i, w := range want {
|
||||
if got := r.Roll(3, 6); got != w {
|
||||
t.Errorf("seed %d: roll(3,6) call %d = %d, want %d", seed, i, got, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rnd(0) must return 0 without stepping the generator: the C macro
|
||||
// short-circuits before evaluating RN, and BEFORE/AFTER depend on it.
|
||||
func TestRndZeroDoesNotStep(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := &Rng{Seed: 42}
|
||||
if got := r.Rnd(0); got != 0 {
|
||||
t.Fatalf("rnd(0) = %d, want 0", got)
|
||||
}
|
||||
|
||||
if r.Seed != 42 {
|
||||
t.Fatalf("rnd(0) stepped the generator: seed = %d, want 42", r.Seed)
|
||||
}
|
||||
}
|
||||
|
||||
// spread(1)==1 and spread(2)==2 deterministically; the C BEFORE/AFTER
|
||||
// constants rely on this.
|
||||
func TestSpreadSmallValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := &RogueGame{Rng: &Rng{Seed: 7}}
|
||||
if got := g.spread(1); got != 1 {
|
||||
t.Errorf("spread(1) = %d, want 1", got)
|
||||
}
|
||||
|
||||
if got := g.spread(2); got != 2 {
|
||||
t.Errorf("spread(2) = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
481
game/rooms.go
481
game/rooms.go
@@ -1,481 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// rooms.c — create the layout for the new level.
|
||||
|
||||
// spot is the rooms.c SPOT position matrix for maze positions.
|
||||
type spot struct {
|
||||
nexits int
|
||||
exits [4]Coord
|
||||
used bool
|
||||
}
|
||||
|
||||
// mazeState is the rooms.c file-scope maze generation state.
|
||||
type mazeState struct {
|
||||
maxy, maxx, starty, startx int
|
||||
maze [NumLines/3 + 1][NumCols/3 + 1]spot
|
||||
}
|
||||
|
||||
const goldGrp = 1
|
||||
|
||||
// digRooms creates rooms and corridors with a connectivity graph (rooms.c
|
||||
// do_rooms).
|
||||
func (g *RogueGame) digRooms() {
|
||||
var bsze Coord // maximum room size
|
||||
|
||||
bsze.X = NumCols / 3
|
||||
bsze.Y = NumLines / 3
|
||||
// Clear things for a new level
|
||||
for i := range g.Level.Rooms {
|
||||
rp := &g.Level.Rooms[i]
|
||||
rp.GoldVal = 0
|
||||
rp.Exits = rp.Exits[:0]
|
||||
rp.Flags = 0
|
||||
}
|
||||
// Put the gone rooms, if any, on the level
|
||||
leftOut := g.rnd(4)
|
||||
for range leftOut {
|
||||
g.Level.Rooms[g.randomRoom()].Flags.Set(Gone)
|
||||
}
|
||||
// dig and populate all the rooms on the level
|
||||
for i := range g.Level.Rooms {
|
||||
g.digRoom(i, bsze)
|
||||
}
|
||||
}
|
||||
|
||||
// digRoom digs and populates one room (the loop body of rooms.c
|
||||
// do_rooms).
|
||||
func (g *RogueGame) digRoom(i int, bsze Coord) {
|
||||
rp := &g.Level.Rooms[i]
|
||||
// Find upper left corner of box that this room goes in
|
||||
top := Coord{X: (i%3)*bsze.X + 1, Y: (i / 3) * bsze.Y}
|
||||
|
||||
if rp.Flags.Has(Gone) {
|
||||
g.placeGoneRoom(rp, top, bsze)
|
||||
|
||||
return
|
||||
}
|
||||
// set room type
|
||||
if g.rnd(10) < g.Depth-1 {
|
||||
rp.Flags.Set(Dark) // dark room
|
||||
|
||||
if g.rnd(15) == 0 {
|
||||
rp.Flags = Maze // maze room
|
||||
}
|
||||
}
|
||||
// Find a place and size for a random room
|
||||
if rp.Flags.Has(Maze) {
|
||||
placeMazeRoom(rp, top, bsze)
|
||||
} else {
|
||||
g.placeNormalRoom(rp, top, bsze)
|
||||
}
|
||||
|
||||
g.drawRoom(rp)
|
||||
g.roomGold(rp)
|
||||
g.roomMonster(rp)
|
||||
}
|
||||
|
||||
// placeGoneRoom places a gone room, making certain that there is a
|
||||
// blank line for passage drawing (rooms.c do_rooms).
|
||||
func (g *RogueGame) placeGoneRoom(rp *Room, top, bsze Coord) {
|
||||
for {
|
||||
rp.Pos.X = top.X + g.rnd(bsze.X-2) + 1
|
||||
rp.Pos.Y = top.Y + g.rnd(bsze.Y-2) + 1
|
||||
rp.Max.X = -NumCols
|
||||
|
||||
rp.Max.Y = -NumLines
|
||||
if rp.Pos.Y > 0 && rp.Pos.Y < NumLines-1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// placeMazeRoom sizes a maze room to fill its box (rooms.c do_rooms).
|
||||
func placeMazeRoom(rp *Room, top, bsze Coord) {
|
||||
rp.Max.X = bsze.X - 1
|
||||
|
||||
rp.Max.Y = bsze.Y - 1
|
||||
if rp.Pos.X = top.X; rp.Pos.X == 1 {
|
||||
rp.Pos.X = 0
|
||||
}
|
||||
|
||||
if rp.Pos.Y = top.Y; rp.Pos.Y == 0 {
|
||||
rp.Pos.Y++
|
||||
rp.Max.Y--
|
||||
}
|
||||
}
|
||||
|
||||
// placeNormalRoom rolls a place and size for an ordinary room (rooms.c
|
||||
// do_rooms).
|
||||
func (g *RogueGame) placeNormalRoom(rp *Room, top, bsze Coord) {
|
||||
for {
|
||||
rp.Max.X = g.rnd(bsze.X-4) + 4
|
||||
rp.Max.Y = g.rnd(bsze.Y-4) + 4
|
||||
rp.Pos.X = top.X + g.rnd(bsze.X-rp.Max.X)
|
||||
|
||||
rp.Pos.Y = top.Y + g.rnd(bsze.Y-rp.Max.Y)
|
||||
if rp.Pos.Y != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// roomGold maybe puts a gold pile in the room (rooms.c do_rooms).
|
||||
func (g *RogueGame) roomGold(rp *Room) {
|
||||
if g.rnd(2) != 0 || (g.HasAmulet && g.Depth < g.MaxDepth) {
|
||||
return
|
||||
}
|
||||
|
||||
gold := newObject()
|
||||
rp.GoldVal = g.goldCalc()
|
||||
gold.GoldValue = rp.GoldVal
|
||||
rp.Gold, _ = g.findFloorIn(rp, 0, false)
|
||||
gold.Pos = rp.Gold
|
||||
g.Level.SetChar(rp.Gold.Y, rp.Gold.X, Gold)
|
||||
|
||||
gold.Flags = Stackable
|
||||
gold.Group = goldGrp
|
||||
gold.Kind = KindGold
|
||||
g.Level.AddObject(gold)
|
||||
}
|
||||
|
||||
// roomMonster maybe puts a monster in the room; gold attracts them
|
||||
// (rooms.c do_rooms).
|
||||
func (g *RogueGame) roomMonster(rp *Room) {
|
||||
prob := 25
|
||||
if rp.GoldVal > 0 {
|
||||
prob = 80
|
||||
}
|
||||
|
||||
if g.rnd(100) < prob {
|
||||
tp := &Monster{}
|
||||
mp, _ := g.findFloorIn(rp, 0, true)
|
||||
g.newMonster(tp, g.randMonster(false), mp)
|
||||
g.givePack(tp)
|
||||
}
|
||||
}
|
||||
|
||||
// drawRoom draws a box around a room and lays down the floor for normal
|
||||
// rooms; for maze rooms, draws the maze (rooms.c draw_room).
|
||||
func (g *RogueGame) drawRoom(rp *Room) {
|
||||
if rp.Flags.Has(Maze) {
|
||||
g.digMaze(rp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.vert(rp, rp.Pos.X) // Draw left side
|
||||
g.vert(rp, rp.Pos.X+rp.Max.X-1) // Draw right side
|
||||
g.horiz(rp, rp.Pos.Y) // Draw top
|
||||
g.horiz(rp, rp.Pos.Y+rp.Max.Y-1) // Draw bottom
|
||||
|
||||
// Put the floor down
|
||||
for y := rp.Pos.Y + 1; y < rp.Pos.Y+rp.Max.Y-1; y++ {
|
||||
for x := rp.Pos.X + 1; x < rp.Pos.X+rp.Max.X-1; x++ {
|
||||
g.Level.SetChar(y, x, Floor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vert draws a vertical line (rooms.c vert).
|
||||
func (g *RogueGame) vert(rp *Room, startx int) {
|
||||
for y := rp.Pos.Y + 1; y <= rp.Max.Y+rp.Pos.Y-1; y++ {
|
||||
g.Level.SetChar(y, startx, '|')
|
||||
}
|
||||
}
|
||||
|
||||
// horiz draws a horizontal line (rooms.c horiz).
|
||||
func (g *RogueGame) horiz(rp *Room, starty int) {
|
||||
for x := rp.Pos.X; x <= rp.Pos.X+rp.Max.X-1; x++ {
|
||||
g.Level.SetChar(starty, x, '-')
|
||||
}
|
||||
}
|
||||
|
||||
// digMaze digs a maze (rooms.c do_maze).
|
||||
func (g *RogueGame) digMaze(rp *Room) {
|
||||
m := &g.maze
|
||||
for y := range m.maze {
|
||||
for x := range m.maze[y] {
|
||||
m.maze[y][x].used = false
|
||||
m.maze[y][x].nexits = 0
|
||||
}
|
||||
}
|
||||
|
||||
m.maxy = rp.Max.Y
|
||||
m.maxx = rp.Max.X
|
||||
m.starty = rp.Pos.Y
|
||||
m.startx = rp.Pos.X
|
||||
starty := (g.rnd(rp.Max.Y) / 2) * 2
|
||||
startx := (g.rnd(rp.Max.X) / 2) * 2
|
||||
pos := Coord{Y: starty + m.starty, X: startx + m.startx}
|
||||
g.putPassage(pos)
|
||||
g.dig(starty, startx)
|
||||
}
|
||||
|
||||
// dig digs out from around where we are now, if possible (rooms.c dig).
|
||||
func (g *RogueGame) dig(y, x int) {
|
||||
m := &g.maze
|
||||
|
||||
for {
|
||||
nexty, nextx, ok := g.digPick(y, x)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
g.accountMaze(y, x, nexty, nextx)
|
||||
g.accountMaze(nexty, nextx, y, x)
|
||||
|
||||
g.putPassage(digWallGap(m, y, x, nexty, nextx))
|
||||
g.putPassage(Coord{Y: nexty + m.starty, X: nextx + m.startx})
|
||||
g.dig(nexty, nextx)
|
||||
}
|
||||
}
|
||||
|
||||
// digPick reservoir-picks the next unvisited maze cell; ok is false
|
||||
// when the digger is boxed in (the candidate scan of rooms.c dig).
|
||||
func (g *RogueGame) digPick(y, x int) (int, int, bool) {
|
||||
m := &g.maze
|
||||
del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}}
|
||||
|
||||
cnt := 0
|
||||
|
||||
var nexty, nextx int
|
||||
|
||||
for _, cp := range del {
|
||||
newy := y + cp.Y
|
||||
|
||||
newx := x + cp.X
|
||||
if newy < 0 || newy > m.maxy || newx < 0 || newx > m.maxx {
|
||||
continue
|
||||
}
|
||||
|
||||
if g.Level.FlagsAt(newy+m.starty, newx+m.startx).Has(FPassage) {
|
||||
continue
|
||||
}
|
||||
|
||||
if cnt++; g.rnd(cnt) == 0 {
|
||||
nexty = newy
|
||||
nextx = newx
|
||||
}
|
||||
}
|
||||
|
||||
return nexty, nextx, cnt != 0
|
||||
}
|
||||
|
||||
// digWallGap picks the wall square to knock out between two maze cells
|
||||
// (rooms.c dig).
|
||||
func digWallGap(m *mazeState, y, x, nexty, nextx int) Coord {
|
||||
if nexty == y {
|
||||
pos := Coord{Y: y + m.starty, X: nextx + m.startx - 1}
|
||||
if nextx-x < 0 {
|
||||
pos.X = nextx + m.startx + 1
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
pos := Coord{X: x + m.startx, Y: nexty + m.starty - 1}
|
||||
if nexty-y < 0 {
|
||||
pos.Y = nexty + m.starty + 1
|
||||
}
|
||||
|
||||
return pos
|
||||
}
|
||||
|
||||
// accountMaze accounts for maze exits (rooms.c accnt_maze).
|
||||
func (g *RogueGame) accountMaze(y, x, ny, nx int) {
|
||||
sp := &g.maze.maze[y][x]
|
||||
for i := range sp.nexits {
|
||||
if sp.exits[i].Y == ny && sp.exits[i].X == nx {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Note: the C original never increments nexits here (a latent bug it
|
||||
// inherits); it stores the exit in the next slot and relies on the
|
||||
// slot staying zero-counted. We reproduce the store-without-count.
|
||||
if sp.nexits < len(sp.exits) {
|
||||
sp.exits[sp.nexits] = Coord{Y: ny, X: nx}
|
||||
}
|
||||
}
|
||||
|
||||
// randomPos picks a random spot in a room (rooms.c rnd_pos).
|
||||
func (g *RogueGame) randomPos(rp *Room) Coord {
|
||||
var cp Coord
|
||||
|
||||
cp.X = rp.Pos.X + g.rnd(rp.Max.X-2) + 1
|
||||
cp.Y = rp.Pos.Y + g.rnd(rp.Max.Y-2) + 1
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
// findFloor finds a valid floor spot, picking a new random room each time
|
||||
// around the loop; it retries forever (rooms.c find_floor with rp == NULL
|
||||
// — every such C call site passed FALSE for the limit).
|
||||
func (g *RogueGame) findFloor(monst bool) (Coord, bool) {
|
||||
return g.findFloorImpl(nil, 0, monst, true)
|
||||
}
|
||||
|
||||
// findFloorIn finds a valid floor spot in this room (rooms.c find_floor
|
||||
// with a specific room).
|
||||
func (g *RogueGame) findFloorIn(rp *Room, limit int, monst bool) (Coord, bool) {
|
||||
return g.findFloorImpl(rp, limit, monst, false)
|
||||
}
|
||||
|
||||
// floorChar is what an empty spot looks like in a room: passage in a
|
||||
// maze, floor otherwise (rooms.c find_floor).
|
||||
func floorChar(rp *Room) byte {
|
||||
if rp.Flags.Has(Maze) {
|
||||
return Passage
|
||||
}
|
||||
|
||||
return Floor
|
||||
}
|
||||
|
||||
func (g *RogueGame) findFloorImpl(
|
||||
rp *Room, limit int, monst, pickroom bool,
|
||||
) (Coord, bool) {
|
||||
var compchar byte
|
||||
if !pickroom {
|
||||
compchar = floorChar(rp)
|
||||
}
|
||||
|
||||
cnt := limit
|
||||
for {
|
||||
if limit != 0 {
|
||||
if cnt--; cnt == -1 {
|
||||
return Coord{}, false
|
||||
}
|
||||
}
|
||||
|
||||
if pickroom {
|
||||
rp = &g.Level.Rooms[g.randomRoom()]
|
||||
compchar = floorChar(rp)
|
||||
}
|
||||
|
||||
cp := g.randomPos(rp)
|
||||
|
||||
pp := g.Level.At(cp.Y, cp.X)
|
||||
if monst {
|
||||
if pp.Monst == nil && stepOk(pp.Ch) {
|
||||
return cp, true
|
||||
}
|
||||
} else if pp.Ch == compchar {
|
||||
return cp, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enterRoom is the code executed whenever you appear in a room (rooms.c
|
||||
// enter_room).
|
||||
func (g *RogueGame) enterRoom(cp Coord) {
|
||||
p := &g.Player
|
||||
rp := g.roomIn(cp)
|
||||
p.Room = rp
|
||||
g.doorOpen(rp)
|
||||
|
||||
if !rp.Flags.Has(Dark) && !p.On(Blind) {
|
||||
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
|
||||
g.move(y, rp.Pos.X)
|
||||
|
||||
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
|
||||
g.enterRoomCell(y, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enterRoomCell draws one square of a room being lit on entry (the loop
|
||||
// body of rooms.c enter_room).
|
||||
func (g *RogueGame) enterRoomCell(y, x int) {
|
||||
tp := g.Level.MonsterAt(y, x)
|
||||
|
||||
ch := g.Level.Char(y, x)
|
||||
if tp == nil {
|
||||
if g.inch() != ch {
|
||||
g.addch(ch)
|
||||
} else {
|
||||
g.move(y, x+1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tp.OldCh = ch
|
||||
if g.seeMonst(tp) {
|
||||
g.addch(tp.Disguise)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
g.standout()
|
||||
g.addch(tp.Disguise)
|
||||
g.standend()
|
||||
} else {
|
||||
g.addch(ch)
|
||||
}
|
||||
}
|
||||
|
||||
// leaveRoom is the code for when we exit a room (rooms.c leave_room).
|
||||
func (g *RogueGame) leaveRoom(cp Coord) {
|
||||
p := &g.Player
|
||||
rp := p.Room
|
||||
|
||||
if rp.Flags.Has(Maze) {
|
||||
return
|
||||
}
|
||||
|
||||
var floor byte
|
||||
|
||||
switch {
|
||||
case rp.Flags.Has(Gone):
|
||||
floor = Passage
|
||||
case !rp.Flags.Has(Dark) || p.On(Blind):
|
||||
floor = Floor
|
||||
default:
|
||||
floor = ' '
|
||||
}
|
||||
|
||||
p.Room = &g.Level.Passages[*g.Level.FlagsAt(cp.Y, cp.X)&FPassNum]
|
||||
for y := rp.Pos.Y; y < rp.Max.Y+rp.Pos.Y; y++ {
|
||||
for x := rp.Pos.X; x < rp.Max.X+rp.Pos.X; x++ {
|
||||
g.leaveRoomCell(floor, y, x)
|
||||
}
|
||||
}
|
||||
|
||||
g.doorOpen(rp)
|
||||
}
|
||||
|
||||
// leaveRoomCell hides one square of a room being left (the loop body of
|
||||
// rooms.c leave_room).
|
||||
func (g *RogueGame) leaveRoomCell(floor byte, y, x int) {
|
||||
g.move(y, x)
|
||||
|
||||
switch ch := g.inch(); ch {
|
||||
case Floor:
|
||||
if floor == ' ' {
|
||||
g.addch(' ')
|
||||
}
|
||||
default:
|
||||
// to check for monster, we have to strip out the standout
|
||||
// bit (our Window returns the bare character already)
|
||||
if !isUpper(ch) {
|
||||
return
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
g.standout()
|
||||
g.addch(ch)
|
||||
g.standend()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
pp := g.Level.At(y, x)
|
||||
if pp.Ch == Door {
|
||||
g.addch(Door)
|
||||
} else {
|
||||
g.addch(floor)
|
||||
}
|
||||
}
|
||||
}
|
||||
211
game/run_test.go
211
game/run_test.go
@@ -1,211 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fortify makes the hero effectively immortal for a crash-sweep drive:
|
||||
// game-over now calls myExit and os.Exit(0) (step 8), which would kill the
|
||||
// test binary, so every death vector is neutralized. Re-applied each turn
|
||||
// because combat, digestion, freezing, and level drain chip away at these.
|
||||
func fortify(g *RogueGame) {
|
||||
p := &g.Player
|
||||
p.Stats.HP = 30000 // survive combat, arrow/dart traps, bolts
|
||||
p.Stats.MaxHP = 30000 // survive vampire max-hp drain
|
||||
p.Stats.Exp = 30000 // survive wraith level drain (death when exp hits 0)
|
||||
p.FoodLeft = 30000 // never starve
|
||||
g.NoCommand = 0 // never freeze to death (ice monster / sleep trap)
|
||||
g.NoMove = 0 // never stay stuck in a bear trap
|
||||
}
|
||||
|
||||
// driveTurns runs the game's per-turn loop up to n times, doing the same
|
||||
// first-level and pre-play setup Run() does. Run() itself no longer
|
||||
// returns — game-over exits the process — so tests drive command()
|
||||
// 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.startLevel()
|
||||
g.prePlay()
|
||||
|
||||
for range n {
|
||||
g.command()
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunDownStairs stands the hero on the staircase and descends via the
|
||||
// '>' command through the real turn loop, then checks the level changed.
|
||||
func TestRunDownStairs(t *testing.T) {
|
||||
t.Parallel()
|
||||
// '>' 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
t.Parallel()
|
||||
|
||||
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
|
||||
|
||||
for y := range NumLines {
|
||||
if strings.Contains(g.scr.Std.Line(y), "Top Ten") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("score list not on screen")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeepPlaythrough drives a fortified hero through the real command loop:
|
||||
// quaff/read/zap on the first level, then descend through the staircase to
|
||||
// depth 8, saving and restoring mid-way. It is a crash sweep of the turn
|
||||
// engine, deep level generation, item effects, and mid-game save/restore.
|
||||
// The hero is fortified so no death exits the process (step 8), and the fixed
|
||||
// seed keeps it deterministic.
|
||||
func TestDeepPlaythrough(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := New(Params{Seed: 4242, Wizard: true, Term: &testTerm{}})
|
||||
g.startLevel()
|
||||
g.prePlay()
|
||||
fortify(g)
|
||||
|
||||
// Stock and use one of each consumable through the command dispatch.
|
||||
pot := give(g, &Object{Kind: KindPotion, Which: int(PotionHealing)})
|
||||
scr := give(g, &Object{Kind: KindScroll, Which: int(ScrollMagicMapping)})
|
||||
|
||||
wand := newObject()
|
||||
wand.Kind = KindWand
|
||||
wand.Which = int(WandLight)
|
||||
wand.Charges = 5
|
||||
zap := give(g, wand)
|
||||
|
||||
setInput(t, g, 'q', pot) // quaff healing
|
||||
g.command()
|
||||
setInput(t, g, 'r', scr) // read magic mapping
|
||||
g.command()
|
||||
setInput(t, g, 'z', 'h', zap) // zap the light wand west
|
||||
g.command()
|
||||
fortify(g)
|
||||
|
||||
// Each consumable identifies itself on use, confirming the q/r/z
|
||||
// commands actually ran through dispatch (not aborted on a bad prompt).
|
||||
if !g.Items.Potions[PotionHealing].Know {
|
||||
t.Error("quaff command did not identify the healing potion")
|
||||
}
|
||||
|
||||
if !g.Items.Scrolls[ScrollMagicMapping].Know {
|
||||
t.Error("read command did not identify the magic-mapping scroll")
|
||||
}
|
||||
|
||||
if !g.Items.Sticks[WandLight].Know {
|
||||
t.Error("zap command did not identify the light wand")
|
||||
}
|
||||
|
||||
const wantDepth = 8
|
||||
|
||||
for g.Depth < wantDepth {
|
||||
g.Player.Pos = g.Level.Stairs // stand on the stairs
|
||||
setInput(t, g, '>', '.') // '>' descends (free), '.' pays the turn
|
||||
g.command()
|
||||
fortify(g)
|
||||
|
||||
if g.Depth == 4 {
|
||||
g = saveAndRestore(t, g)
|
||||
fortify(g)
|
||||
}
|
||||
}
|
||||
|
||||
if g.Depth != wantDepth {
|
||||
t.Errorf("depth = %d after descending, want %d", g.Depth, wantDepth)
|
||||
}
|
||||
|
||||
if g.Player.Stats.HP <= 0 {
|
||||
t.Error("hero died during the playthrough")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTurnLoopCrashSweep mashes movement, search, and rest through the real
|
||||
// turn loop for many turns on several seeds, exercising combat, monster AI,
|
||||
// and traps. The hero is fortified each turn so nothing exits the process,
|
||||
// and the fixed seeds keep it deterministic; the point is to surface panics.
|
||||
func TestTurnLoopCrashSweep(t *testing.T) {
|
||||
t.Parallel()
|
||||
// A generous mix of movement, search, and rest. The spaces between
|
||||
// commands double as answers to any --More-- prompt (wait_for eats
|
||||
// everything up to a space); without them one prompt would swallow the
|
||||
// rest of the script. The script is long enough that the bounded drive
|
||||
// never exhausts it (which would spin on the auto-fed prompt input).
|
||||
script := []byte(strings.Repeat("h j k l y u b n s . ", 400))
|
||||
|
||||
for _, seed := range []int32{1, 99, 2026, 31337} {
|
||||
g := New(Params{Seed: seed, Term: &testTerm{input: script}})
|
||||
g.startLevel()
|
||||
g.prePlay()
|
||||
|
||||
for range 200 {
|
||||
fortify(g)
|
||||
g.command()
|
||||
}
|
||||
|
||||
if g.Player.Stats.HP <= 0 {
|
||||
t.Errorf("seed %d: hero died despite fortify", seed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveAndRestore snapshots the game to a file, restores it, checks the key
|
||||
// state survived, and returns the restored game ready to keep playing.
|
||||
func saveAndRestore(t *testing.T, g *RogueGame) *RogueGame {
|
||||
t.Helper()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "deep.save")
|
||||
|
||||
saveErr := g.saveFile(path)
|
||||
if saveErr != nil {
|
||||
t.Fatalf("saveFile: %v", saveErr)
|
||||
}
|
||||
|
||||
h, err := Restore(path, Params{Wizard: true, Term: &testTerm{}})
|
||||
if err != nil {
|
||||
t.Fatalf("Restore: %v", err)
|
||||
}
|
||||
|
||||
if h.Depth != g.Depth || h.Player.Purse != g.Player.Purse {
|
||||
t.Errorf("restored game diverged: depth %d/%d purse %d/%d",
|
||||
h.Depth, g.Depth, h.Player.Purse, g.Player.Purse)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
934
game/save.go
934
game/save.go
@@ -1,934 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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.
|
||||
|
||||
// saveFormatVersion identifies the snapshot layout; it changes whenever a
|
||||
// field rename or retype would make gob silently mis-decode an older
|
||||
// save ("go2": Object.Type became Kind ObjectKind; "go3": Object.Arm split
|
||||
// into ArmorClass/Charges/GoldValue/Bonus, damage strings became
|
||||
// DiceSpec).
|
||||
const saveFormatVersion = Release + "-go3"
|
||||
|
||||
// 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
|
||||
TrapCount 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 {
|
||||
st := g.snapshotHeader()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
st.Player = g.snapshotPlayer()
|
||||
|
||||
// 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)
|
||||
st.Dests = append(st.Dests, g.destRefFor(m, objAt))
|
||||
}
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
// snapshotHeader captures the scalar game state (the field list of
|
||||
// state.c rs_save_file).
|
||||
func (g *RogueGame) snapshotHeader() *SaveState {
|
||||
return &SaveState{
|
||||
Version: saveFormatVersion,
|
||||
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,
|
||||
TrapCount: g.Level.TrapCount,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
// snapshotPlayer captures the player, equipment as pack indices (the
|
||||
// player half of snapshot).
|
||||
func (g *RogueGame) snapshotPlayer() savedPlayer {
|
||||
p := &g.Player
|
||||
|
||||
sp := 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 {
|
||||
sp.Body.Pack = append(sp.Body.Pack, *o)
|
||||
}
|
||||
|
||||
return sp
|
||||
}
|
||||
|
||||
// destRefFor encodes a monster's chase target as a (kind, index)
|
||||
// reference: the hero, another monster, a level object, or room gold
|
||||
// (state.c rs_write_thing).
|
||||
func (g *RogueGame) destRefFor(m *Monster, objAt map[*Object]int) destRef {
|
||||
switch {
|
||||
case m.Dest == nil:
|
||||
return destRef{}
|
||||
case m.Dest == &g.Player.Pos:
|
||||
return destRef{Kind: 1}
|
||||
}
|
||||
|
||||
for mi, om := range g.Level.Monsters {
|
||||
if m.Dest == &om.Pos {
|
||||
return destRef{Kind: 2, Idx: mi}
|
||||
}
|
||||
}
|
||||
|
||||
for _, oo := range g.Level.Objects {
|
||||
if m.Dest == &oo.Pos {
|
||||
return destRef{Kind: 3, Idx: objAt[oo]}
|
||||
}
|
||||
}
|
||||
|
||||
for ri := range g.Level.Rooms {
|
||||
if m.Dest == &g.Level.Rooms[ri].Gold {
|
||||
return destRef{Kind: 4, Idx: ri}
|
||||
}
|
||||
}
|
||||
|
||||
return destRef{}
|
||||
}
|
||||
|
||||
// applySnapshot rebuilds live game state from a snapshot.
|
||||
func (g *RogueGame) applySnapshot(st *SaveState) {
|
||||
g.applyHeader(st)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
g.applyPlayer(st)
|
||||
g.applyMonsters(st)
|
||||
g.applyDests(st)
|
||||
|
||||
g.scr.Std.SetContents(st.Screen)
|
||||
}
|
||||
|
||||
// applyHeader restores the scalar game state (the field list of
|
||||
// applySnapshot).
|
||||
func (g *RogueGame) applyHeader(st *SaveState) {
|
||||
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.TrapCount = st.TrapCount
|
||||
g.applyTurnState(st)
|
||||
}
|
||||
|
||||
// applyTurnState restores the in-turn command state (the second half of
|
||||
// applyHeader).
|
||||
func (g *RogueGame) applyTurnState(st *SaveState) {
|
||||
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
|
||||
}
|
||||
|
||||
// applyPlayer restores the player, resolving equipment pack indices
|
||||
// (the player half of applySnapshot).
|
||||
func (g *RogueGame) applyPlayer(st *SaveState) {
|
||||
p := &g.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
|
||||
}
|
||||
|
||||
// applyMonsters rebuilds the monster list and its map index from a
|
||||
// snapshot (the monster half of applySnapshot).
|
||||
func (g *RogueGame) applyMonsters(st *SaveState) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// applyDests re-aims the monsters' chase targets from their (kind,
|
||||
// index) references (the fixup half of applySnapshot).
|
||||
func (g *RogueGame) applyDests(st *SaveState) {
|
||||
for i, ref := range st.Dests {
|
||||
m := g.Level.Monsters[i]
|
||||
|
||||
switch ref.Kind {
|
||||
case 1:
|
||||
m.Dest = &g.Player.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveAnswer is a yes/no/escape prompt result in the save-game flow.
|
||||
type saveAnswer int
|
||||
|
||||
// The saveGame prompt outcomes.
|
||||
const (
|
||||
saveYes saveAnswer = iota
|
||||
saveNo
|
||||
saveAbort
|
||||
)
|
||||
|
||||
// saveGame implements the "save game" command (save.c save_game). The
|
||||
// labeled prompt loop stands in for the C goto over/gotfile flow.
|
||||
func (g *RogueGame) saveGame() {
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
prompt:
|
||||
for {
|
||||
useDefault := false
|
||||
|
||||
if g.FileName != "" {
|
||||
a := g.askDefaultSave()
|
||||
if a == saveAbort {
|
||||
return
|
||||
}
|
||||
|
||||
useDefault = a == saveYes
|
||||
}
|
||||
|
||||
for {
|
||||
buf, ok := g.saveFileName(useDefault)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
useDefault = false
|
||||
|
||||
a := g.saveCheckOverwrite(buf)
|
||||
if a == saveAbort {
|
||||
return
|
||||
}
|
||||
|
||||
if a == saveNo {
|
||||
continue prompt // the C goto over: start again
|
||||
}
|
||||
|
||||
g.FileName = buf
|
||||
|
||||
err := g.saveFile(g.FileName)
|
||||
if err != nil {
|
||||
g.msg("%s", err.Error())
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break prompt
|
||||
}
|
||||
}
|
||||
|
||||
g.myExit()
|
||||
}
|
||||
|
||||
// askDefaultSave asks whether to save to the current file name (save.c
|
||||
// save_game).
|
||||
func (g *RogueGame) askDefaultSave() saveAnswer {
|
||||
for {
|
||||
g.msg("save file (%s)? ", g.FileName)
|
||||
c := g.readchar()
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
switch c {
|
||||
case Escape:
|
||||
g.msg("")
|
||||
|
||||
return saveAbort
|
||||
case 'y', 'Y':
|
||||
g.addstr("Yes\n")
|
||||
g.refresh()
|
||||
|
||||
return saveYes
|
||||
case 'n', 'N':
|
||||
return saveNo
|
||||
}
|
||||
|
||||
g.msg("please answer Y or N")
|
||||
}
|
||||
}
|
||||
|
||||
// saveFileName picks the save path: the default, or a prompted one; ok
|
||||
// is false when the player quit the prompt (save.c save_game).
|
||||
func (g *RogueGame) saveFileName(useDefault bool) (string, bool) {
|
||||
if useDefault {
|
||||
return g.FileName, true
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("file name: ")
|
||||
|
||||
buf := ""
|
||||
if g.getStr(&buf, g.scr.Std) == Quit {
|
||||
g.msg("")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
return buf, true
|
||||
}
|
||||
|
||||
// saveCheckOverwrite guards an existing file: saveNo restarts the whole
|
||||
// prompt, saveAbort quits (save.c save_game).
|
||||
func (g *RogueGame) saveCheckOverwrite(buf string) saveAnswer {
|
||||
// test to see if the file exists
|
||||
_, statErr := os.Stat(buf)
|
||||
if statErr != nil {
|
||||
return saveYes
|
||||
}
|
||||
|
||||
answer := g.askOverwrite()
|
||||
if answer != saveYes {
|
||||
return answer
|
||||
}
|
||||
|
||||
g.msg("file name: %s", buf)
|
||||
_ = os.Remove(g.FileName) // best effort, as in C (md_unlink)
|
||||
|
||||
return saveYes
|
||||
}
|
||||
|
||||
// askOverwrite asks whether to overwrite the existing file (save.c
|
||||
// save_game).
|
||||
func (g *RogueGame) askOverwrite() saveAnswer {
|
||||
for {
|
||||
g.msg("File exists. Do you wish to overwrite it?")
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
switch g.readchar() {
|
||||
case Escape:
|
||||
g.msg("")
|
||||
|
||||
return saveAbort
|
||||
case 'y', 'Y':
|
||||
return saveYes
|
||||
case 'n', 'N':
|
||||
return saveNo
|
||||
}
|
||||
|
||||
g.msg("Please answer Y or N")
|
||||
}
|
||||
}
|
||||
|
||||
// saveFile writes the saved game (save.c save_file).
|
||||
//
|
||||
// The snapshot goes to a temporary file in the target's own directory and
|
||||
// is renamed over the target, so there is no instant at which the player
|
||||
// has no save file: until the rename the old file is whole, and after it
|
||||
// the new one is. C wrote straight over the target, and this port did the
|
||||
// same with a remove in front of it (AutoSave), so a write that failed —
|
||||
// or a signal-time save cut short by the process dying — could leave the
|
||||
// player with neither the old save nor a usable new one (issue #24).
|
||||
//
|
||||
// The temporary file is fsynced before the rename so its contents reach
|
||||
// the disk ahead of the directory entry that will point at it. The
|
||||
// directory itself is not fsynced: that would only matter for a machine
|
||||
// that loses power in the same instant, and the old save survives that
|
||||
// case anyway. A process killed mid-encode leaves its temporary file
|
||||
// behind, which is litter next to a destroyed save file, and the dot
|
||||
// prefix keeps it out of the way.
|
||||
func (g *RogueGame) saveFile(path string) error {
|
||||
f, err := os.CreateTemp(filepath.Dir(path), ".rogue-save-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmp := f.Name()
|
||||
|
||||
writeErr := writeSnapshotFile(f, g.snapshot())
|
||||
if writeErr != nil {
|
||||
_ = os.Remove(tmp) // never leave a half-written file behind
|
||||
|
||||
return writeErr
|
||||
}
|
||||
|
||||
renErr := os.Rename(tmp, path)
|
||||
if renErr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
|
||||
return renErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeSnapshotFile writes the snapshot into an open temporary file: it
|
||||
// encodes, fsyncs so the bytes reach the disk before the caller renames
|
||||
// the file into place, chmods it read-only as the C game's saves were
|
||||
// (save.c save_file), and closes it. It never removes the file: its
|
||||
// caller owns the cleanup, so that one place decides what happens to a
|
||||
// failed write.
|
||||
func writeSnapshotFile(f *os.File, st *SaveState) error {
|
||||
encErr := gob.NewEncoder(f).Encode(st)
|
||||
if encErr == nil {
|
||||
encErr = f.Sync()
|
||||
}
|
||||
|
||||
if encErr == nil {
|
||||
encErr = f.Chmod(0o400)
|
||||
}
|
||||
|
||||
closeErr := f.Close()
|
||||
|
||||
if encErr != nil {
|
||||
return encErr
|
||||
}
|
||||
|
||||
return closeErr
|
||||
}
|
||||
|
||||
// autoSaveRequest is one signal-triggered autosave in flight: the signal
|
||||
// goroutine posts it and waits, the game goroutine performs the save and
|
||||
// closes done. ok is written before done is closed and read only after,
|
||||
// so the close is the happens-before edge that publishes it.
|
||||
type autoSaveRequest struct {
|
||||
done chan struct{}
|
||||
ok bool
|
||||
}
|
||||
|
||||
// AutoSaveOnSignal asks the game goroutine to autosave and waits up to
|
||||
// timeout for it to finish, reporting whether the save actually ran
|
||||
// (save.c auto_save, the SIGHUP/SIGTERM handler). It is the only entry
|
||||
// point the signal goroutine may use, and it deliberately touches no game
|
||||
// state: the gob encoder used to walk the live game tree from the signal
|
||||
// goroutine while the game goroutine was mid-turn mutating it (issue
|
||||
// #24).
|
||||
//
|
||||
// Blocked on input is the case that matters, since a dropped connection
|
||||
// is the whole reason the handler exists: the request is posted first and
|
||||
// the input read is then interrupted, so a game goroutine parked in
|
||||
// ReadChar wakes, saves in readchar, and reads again. A game goroutine
|
||||
// that is running turns instead picks the request up between turns, in
|
||||
// command; one parked in the `!` shell escape picks it up in
|
||||
// runShellEscape.
|
||||
//
|
||||
// The wait is bounded because the signal goroutine's job is to get the
|
||||
// process out. If the game goroutine is somewhere with no service point
|
||||
// at all, the deadline expires, this reports false, and the caller
|
||||
// restores the terminal and exits — leaving the player's previous save
|
||||
// file exactly as it was, which is the point of the rename in saveFile.
|
||||
func (g *RogueGame) AutoSaveOnSignal(timeout time.Duration) bool {
|
||||
req := &autoSaveRequest{done: make(chan struct{})}
|
||||
|
||||
select {
|
||||
case g.sigSave <- req:
|
||||
default:
|
||||
// A request is already queued and unserviced, or there is no
|
||||
// game loop to service one; either way this one would not be
|
||||
// answered either.
|
||||
return false
|
||||
}
|
||||
|
||||
g.scr.Interrupt()
|
||||
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-req.done:
|
||||
return req.ok
|
||||
case <-timer.C:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAutoSaveRequest performs a pending signal-triggered autosave, if
|
||||
// one is waiting, and otherwise returns at once. It runs on the game
|
||||
// goroutine — that is the whole design — so it must only be called where
|
||||
// that goroutine is not itself inside the encode: between turns, or while
|
||||
// parked waiting for input or for the shell escape.
|
||||
//
|
||||
// What is guaranteed, exactly: the encode runs on the one goroutine that
|
||||
// owns the state, so the snapshot is internally consistent and always
|
||||
// restorable. It is *not* guaranteed to be a between-commands snapshot.
|
||||
// Only one of the three service points gives that: the check at the top
|
||||
// of command, which runs after the previous command returned and before
|
||||
// this turn's DoDaemons(Before)/DoFuses(Before). The other two are both
|
||||
// reached from inside a command call already under way, and both cost
|
||||
// the same on restore.
|
||||
//
|
||||
// readchar is reached from prompts raised part-way through a command —
|
||||
// --More-- on the second message of a turn, askOverwrite, getStr, the
|
||||
// direction and pack prompts — and by then the command has already
|
||||
// mutated state: fight sets g.Count and g.Quiet and runs runTo before
|
||||
// any message, revealXeroc writes tp.Disguise before emitting one. The
|
||||
// ordinary top-of-turn key read in readCommand is inside command too,
|
||||
// after that turn's BEFORE daemons and turnUpkeep.
|
||||
//
|
||||
// runShellEscape is no safer. shell is an ordinary command handler ('!'
|
||||
// in the tables.go dispatch table), reached through executeCommand, so a
|
||||
// goroutine parked in the shell escape has already run this turn's
|
||||
// DoDaemons(Before), DoFuses(Before), turnUpkeep and the last-command
|
||||
// bookkeeping, and has not yet run DoDaemons(After), DoFuses(After) or
|
||||
// ringTurnEffects.
|
||||
//
|
||||
// The cost, at both: restoring re-enters playit at the top of command,
|
||||
// so the rest of that command never runs — its AFTER daemons and fuses
|
||||
// and its ring effects are lost — and the restored game opens with a
|
||||
// fresh BEFORE pass on top of the one already in the snapshot. That
|
||||
// second BEFORE pass is not free: rollwand, a live Before daemon once
|
||||
// swander has fired, ticks again and draws from the RNG every fourth
|
||||
// tick, and any Before fuse is decremented again.
|
||||
//
|
||||
// Not every consequence of that pass is shared by both, though. visuals
|
||||
// returns immediately unless g.After, and After is part of the snapshot,
|
||||
// so DVisuals never re-ticks after a shell-escape save: shell sets
|
||||
// g.After = false as its first statement, before it parks. After a
|
||||
// readchar save it usually does re-tick, because turnUpkeep sets
|
||||
// g.After = true just before the top-of-turn read; the exception is a
|
||||
// handler that clears After before prompting, as identifyTrapCommand
|
||||
// does ahead of promptDirection.
|
||||
//
|
||||
// The result is still a coherent game state, one turn's worth of effects
|
||||
// off — strictly better than the torn encode this replaced, and the cost
|
||||
// of being able to save a player whose line dropped mid-prompt, or who
|
||||
// is away in a shell, at all.
|
||||
func (g *RogueGame) serviceAutoSaveRequest() {
|
||||
select {
|
||||
case req := <-g.sigSave:
|
||||
g.runAutoSaveRequest(req)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// runAutoSaveRequest answers one request: save, then release the waiter.
|
||||
func (g *RogueGame) runAutoSaveRequest(req *autoSaveRequest) {
|
||||
req.ok = g.autoSave()
|
||||
|
||||
close(req.done)
|
||||
}
|
||||
|
||||
// autoSave silently saves to the current file name (save.c auto_save),
|
||||
// reporting whether it wrote a save. Game-goroutine only — reach it
|
||||
// through AutoSaveOnSignal from anywhere else.
|
||||
//
|
||||
// The error is not surfaced: there is no player to tell, since the
|
||||
// terminal is on its way out, and nothing sensible to do about it. It is
|
||||
// reported to the waiting signal goroutine as a failed save rather than
|
||||
// discarded outright, which is what the old `_ =` here used to do.
|
||||
func (g *RogueGame) autoSave() bool {
|
||||
if g.FileName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return g.saveFile(g.FileName) == nil
|
||||
}
|
||||
|
||||
// ErrSaveOutOfDate reports a save file from an incompatible version.
|
||||
var ErrSaveOutOfDate = errors.New("sorry, saved game is out of date")
|
||||
|
||||
// ErrSaveCorrupt reports a save file whose contents are structurally
|
||||
// impossible for a game this code could have written.
|
||||
var ErrSaveCorrupt = errors.New("sorry, saved game is corrupt")
|
||||
|
||||
// validateSnapshotObjects rejects a snapshot carrying an item whose Which
|
||||
// would index past the end of its kind's tables. The file is written by
|
||||
// this program, so such a value can only come from corruption or
|
||||
// tampering; refusing it at the door is what keeps a malformed object
|
||||
// from reaching the effect tables in doZap, quaff, and readScroll, where
|
||||
// the wizard-create bug (issue #10) used to put one.
|
||||
func validateSnapshotObjects(st *SaveState) error {
|
||||
lists := make([][]Object, 0, 2+len(st.Monsters))
|
||||
lists = append(lists, st.Objects, st.Player.Body.Pack)
|
||||
|
||||
for i := range st.Monsters {
|
||||
lists = append(lists, st.Monsters[i].Pack)
|
||||
}
|
||||
|
||||
for _, list := range lists {
|
||||
for i := range list {
|
||||
obj := &list[i]
|
||||
if !obj.hasValidWhich() {
|
||||
return fmt.Errorf("%w: %s has out-of-range which %d",
|
||||
ErrSaveCorrupt, obj.Kind, obj.Which)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, params Params) (*RogueGame, error) {
|
||||
f, err := os.Open(path) //nolint:gosec // G304: the save path is user-chosen by design
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = f.Close() }() // read-only handle
|
||||
|
||||
var st SaveState
|
||||
|
||||
decErr := gob.NewDecoder(f).Decode(&st)
|
||||
if decErr != nil {
|
||||
return nil, fmt.Errorf("%s: corrupt or incompatible save file: %w",
|
||||
path, decErr)
|
||||
}
|
||||
|
||||
if st.Version != saveFormatVersion {
|
||||
return nil, ErrSaveOutOfDate
|
||||
}
|
||||
|
||||
valErr := validateSnapshotObjects(&st)
|
||||
if valErr != nil {
|
||||
return nil, valErr
|
||||
}
|
||||
|
||||
g := &RogueGame{
|
||||
data: newGameData(),
|
||||
Rng: &Rng{},
|
||||
Playing: true,
|
||||
ScorePath: params.ScorePath,
|
||||
FileName: path,
|
||||
rogueOpts: params.RogueOpts,
|
||||
restored: true,
|
||||
sigSave: make(chan *autoSaveRequest, 1),
|
||||
}
|
||||
g.scr = NewScreen(params.Term)
|
||||
g.Msgs.attach(g.scr, g.look, g.readchar)
|
||||
g.applySnapshot(&st)
|
||||
|
||||
// defeat multiple restarting from the same place
|
||||
rmErr := os.Remove(path)
|
||||
if rmErr != nil {
|
||||
return nil, fmt.Errorf("cannot unlink file: %w", rmErr)
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSaveRestoreRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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[PotionHealing].Know = true
|
||||
g.Items.Scrolls[ScrollMagicMapping].Guess = "map???"
|
||||
|
||||
g.Monsters['F'-'A'].Stats.Dmg = dice("3x1") // mutated bestiary must survive
|
||||
if len(g.Level.Monsters) > 0 {
|
||||
g.Level.Monsters[0].Flags.Set(Awake)
|
||||
g.Level.Monsters[0].Dest = &g.Player.Pos
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "rogue.save")
|
||||
|
||||
saveErr := g.saveFile(path)
|
||||
if saveErr != nil {
|
||||
t.Fatalf("saveFile: %v", saveErr)
|
||||
}
|
||||
|
||||
h, err := Restore(path, Params{Term: &testTerm{}})
|
||||
if err != nil {
|
||||
t.Fatalf("Restore: %v", err)
|
||||
}
|
||||
|
||||
_, statErr := os.Stat(path)
|
||||
if !os.IsNotExist(statErr) {
|
||||
t.Error("save file not deleted on restore (C anti-restart rule)")
|
||||
}
|
||||
|
||||
checkRestoredState(t, g, h)
|
||||
checkRestoredMonsters(t, g, h)
|
||||
checkEquipmentAliasing(t, g, h)
|
||||
}
|
||||
|
||||
// checkRestoredState verifies the scalar state survived the round trip.
|
||||
func checkRestoredState(t *testing.T, g, h *RogueGame) {
|
||||
t.Helper()
|
||||
|
||||
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[PotionHealing].Know {
|
||||
t.Error("potion identification lost")
|
||||
}
|
||||
|
||||
if h.Items.Scrolls[ScrollMagicMapping].Guess != "map???" {
|
||||
t.Error("scroll guess lost")
|
||||
}
|
||||
|
||||
if h.Monsters['F'-'A'].Stats.Dmg.String() != "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")
|
||||
}
|
||||
}
|
||||
|
||||
// checkRestoredMonsters verifies the monster list and its pointer fixups
|
||||
// survived the round trip.
|
||||
func checkRestoredMonsters(t *testing.T, g, h *RogueGame) {
|
||||
t.Helper()
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// checkEquipmentAliasing verifies the wielded mace is the same *Object as
|
||||
// the one in the pack.
|
||||
func checkEquipmentAliasing(t *testing.T, g, h *RogueGame) {
|
||||
t.Helper()
|
||||
|
||||
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=%v which=%d", i, o, o.Kind, 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) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 1)
|
||||
path := filepath.Join(t.TempDir(), "rogue.save")
|
||||
st := g.snapshot()
|
||||
st.Version = "0.0.0"
|
||||
|
||||
f, err := os.Create(path) //nolint:gosec // G304: test temp path
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
encErr := gob.NewEncoder(f).Encode(st)
|
||||
if encErr != nil {
|
||||
t.Fatal(encErr)
|
||||
}
|
||||
|
||||
closeErr := f.Close()
|
||||
if closeErr != nil {
|
||||
t.Fatal(closeErr)
|
||||
}
|
||||
|
||||
_, restoreErr := Restore(path, Params{})
|
||||
if restoreErr == nil {
|
||||
t.Error("restore accepted an out-of-date save")
|
||||
}
|
||||
}
|
||||
260
game/score.go
260
game/score.go
@@ -1,260 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// score.h / rip.c score() / save.c rd_score+wr_score — the scoreboard.
|
||||
// The C scorefile was XOR-encrypted structs; the port stores a gob list at
|
||||
// ScorePath (empty path disables persistence but still shows the list).
|
||||
|
||||
// numScores is the C numscores default: the top ten.
|
||||
const numScores = 10
|
||||
|
||||
// ScoreEnt is score.h struct sc_ent.
|
||||
type ScoreEnt struct {
|
||||
UID int
|
||||
Score int
|
||||
Flags int // 0=killed, 1=quit, 2=winner, 3=killed with amulet
|
||||
Monster byte
|
||||
Name string
|
||||
Level int
|
||||
Time int64
|
||||
}
|
||||
|
||||
// rdScore reads the scoreboard file (save.c rd_score).
|
||||
func (g *RogueGame) rdScore() []ScoreEnt {
|
||||
topTen := make([]ScoreEnt, numScores)
|
||||
if g.ScorePath == "" {
|
||||
return topTen
|
||||
}
|
||||
|
||||
f, err := os.Open(g.ScorePath)
|
||||
if err != nil {
|
||||
return topTen
|
||||
}
|
||||
|
||||
defer func() { _ = f.Close() }() // read-only handle
|
||||
|
||||
var onDisk []ScoreEnt
|
||||
|
||||
decErr := gob.NewDecoder(f).Decode(&onDisk)
|
||||
if decErr != nil {
|
||||
return topTen // unreadable scoreboard reads as empty, as in C
|
||||
}
|
||||
|
||||
copy(topTen, onDisk)
|
||||
|
||||
return topTen
|
||||
}
|
||||
|
||||
// wrScore writes the scoreboard file (save.c wr_score).
|
||||
func (g *RogueGame) wrScore(topTen []ScoreEnt) {
|
||||
if g.ScorePath == "" {
|
||||
return
|
||||
}
|
||||
// lock_sc/unlock_sc: exclusive-create lock file with stale takeover.
|
||||
// The whole scoreboard write is best effort, as it was in C: a shared
|
||||
// scoreboard must never take the game down.
|
||||
lock := g.ScorePath + ".lck"
|
||||
for range 5 {
|
||||
lf, err := os.OpenFile(lock, //nolint:gosec // G304: configured path
|
||||
os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
||||
if err == nil {
|
||||
_ = lf.Close()
|
||||
|
||||
defer func() { _ = os.Remove(lock) }()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
fi, statErr := os.Stat(lock)
|
||||
if statErr == nil && time.Since(fi.ModTime()) > staleLockAge {
|
||||
_ = os.Remove(lock)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(g.ScorePath,
|
||||
os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = gob.NewEncoder(f).Encode(topTen)
|
||||
_ = f.Close()
|
||||
}
|
||||
|
||||
// staleLockAge is how old a scoreboard lock file may be before another
|
||||
// process assumes its owner died and takes it over (mach_dep.c lock_sc
|
||||
// aged its lock the same way).
|
||||
const staleLockAge = 10 * time.Second
|
||||
|
||||
// score figures the score and posts it (rip.c score). flags -1 means just
|
||||
// display the list (the -s command line option).
|
||||
func (g *RogueGame) score(amount, flags int, monst byte) {
|
||||
if flags >= 0 && g.scr != nil && g.scr.term != nil {
|
||||
g.mvaddstr(NumLines-1, 0, "[Press return to continue]")
|
||||
g.refresh()
|
||||
g.waitFor('\n')
|
||||
}
|
||||
|
||||
topTen := g.rdScore()
|
||||
// Insert her in list if need be
|
||||
ins := -1
|
||||
if !g.NoScore && flags >= 0 {
|
||||
ins = g.scoreInsert(topTen, amount, flags, monst)
|
||||
}
|
||||
|
||||
lines, highlight := g.scoreLines(topTen, ins)
|
||||
g.showScores(lines, highlight)
|
||||
|
||||
// Update the list file
|
||||
if ins >= 0 {
|
||||
g.wrScore(topTen)
|
||||
}
|
||||
}
|
||||
|
||||
// scoreInsert slots the new score into the top ten, honoring the
|
||||
// one-score-per-losing-uid rule; -1 means it did not place (the
|
||||
// insertion half of rip.c score).
|
||||
func (g *RogueGame) scoreInsert(topTen []ScoreEnt, amount, flags int, monst byte) int {
|
||||
uid := os.Getuid()
|
||||
|
||||
scp := g.scoreSlot(topTen, amount, flags, uid)
|
||||
if scp >= len(topTen) {
|
||||
return -1
|
||||
}
|
||||
|
||||
sc2 := len(topTen) - 1
|
||||
if flags != 2 && !g.AllScore {
|
||||
for i := scp; i < len(topTen); i++ {
|
||||
if topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
sc2 = i
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for sc2 > scp {
|
||||
topTen[sc2] = topTen[sc2-1]
|
||||
sc2--
|
||||
}
|
||||
|
||||
lvl := g.Depth
|
||||
if flags == 2 {
|
||||
lvl = g.MaxDepth
|
||||
}
|
||||
|
||||
topTen[scp] = ScoreEnt{
|
||||
UID: uid,
|
||||
Score: amount,
|
||||
Flags: flags,
|
||||
Monster: monst,
|
||||
Name: g.Whoami,
|
||||
Level: lvl,
|
||||
Time: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return scp
|
||||
}
|
||||
|
||||
// scoreSlot finds where the new score lands: len(topTen) when it does
|
||||
// not place, or when this uid already holds a losing score (the scan of
|
||||
// rip.c score).
|
||||
func (g *RogueGame) scoreSlot(topTen []ScoreEnt, amount, flags, uid int) int {
|
||||
for i := range topTen {
|
||||
if amount > topTen[i].Score {
|
||||
return i
|
||||
}
|
||||
|
||||
if !g.AllScore && flags != 2 &&
|
||||
topTen[i].UID == uid && topTen[i].Flags != 2 {
|
||||
// only one score per nowin uid
|
||||
return len(topTen)
|
||||
}
|
||||
}
|
||||
|
||||
return len(topTen)
|
||||
}
|
||||
|
||||
// scoreLines formats the scoreboard, noting which display line holds
|
||||
// the freshly inserted score (the display half of rip.c score).
|
||||
func (g *RogueGame) scoreLines(topTen []ScoreEnt, ins int) ([]string, int) {
|
||||
label := "Rogueists"
|
||||
if g.AllScore {
|
||||
label = "Scores"
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
fmt.Sprintf("Top Ten %s:", label),
|
||||
" Score Name",
|
||||
}
|
||||
highlight := -1
|
||||
|
||||
for i := range topTen {
|
||||
scp := &topTen[i]
|
||||
if scp.Score == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1,
|
||||
scp.Score, scp.Name, g.data.scoreReasons[scp.Flags], scp.Level)
|
||||
if scp.Flags == 0 || scp.Flags == 3 {
|
||||
line += " by " + g.killname(scp.Monster, true)
|
||||
}
|
||||
|
||||
line += "."
|
||||
|
||||
if i == ins {
|
||||
highlight = len(lines)
|
||||
}
|
||||
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
return lines, highlight
|
||||
}
|
||||
|
||||
// showScores prints the scoreboard on the screen when there is one,
|
||||
// else to standard output (rip.c score).
|
||||
func (g *RogueGame) showScores(lines []string, highlight int) {
|
||||
if g.scr == nil || g.scr.term == nil {
|
||||
for _, line := range lines {
|
||||
_, _ = fmt.Fprintln(os.Stdout, line) // CLI output
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
g.clear()
|
||||
|
||||
for i, line := range lines {
|
||||
if i == highlight {
|
||||
g.standout()
|
||||
}
|
||||
|
||||
g.mvaddstr(i, 0, line)
|
||||
|
||||
if i == highlight {
|
||||
g.standend()
|
||||
}
|
||||
}
|
||||
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
// ShowScores implements the -s command line option: print the scoreboard
|
||||
// and nothing else.
|
||||
func (g *RogueGame) ShowScores() {
|
||||
g.NoScore = true
|
||||
g.score(0, -1, 0)
|
||||
}
|
||||
283
game/screen.go
283
game/screen.go
@@ -1,283 +0,0 @@
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// The screen layer replaces curses. Game code draws into Window cell
|
||||
// buffers (stdscr and the hw scratch window); a Terminal implementation
|
||||
// blits them to a real device. Tests run with a scripted Terminal (or none
|
||||
// at all), which is also how the "screen is a data structure" idiom —
|
||||
// C code reading back what it drew with inch() — stays intact headlessly.
|
||||
|
||||
// Terminal is the physical device: a tcell screen in the real game, a
|
||||
// script in tests.
|
||||
type Terminal interface {
|
||||
// Render blits the window to the device.
|
||||
Render(w *Window)
|
||||
// ReadChar blocks for the next key, translated to Rogue's input bytes
|
||||
// (arrows become hjkl, control keys their C0 codes). ok is false when
|
||||
// the read was woken by Interrupt instead of by a key, which is how a
|
||||
// signal-triggered autosave reaches a game parked on input; the byte
|
||||
// is meaningless then.
|
||||
ReadChar() (ch byte, ok bool)
|
||||
// Interrupt wakes a ReadChar that is blocked waiting for a key. It is
|
||||
// the one Terminal method called from another goroutine, so an
|
||||
// implementation must be safe to call concurrently with ReadChar.
|
||||
Interrupt()
|
||||
// Repaint forces the device to redraw every cell it is showing, the
|
||||
// redraw command's whole point (curses clearok(curscr, TRUE) followed
|
||||
// by wrefresh(curscr)). Render cannot stand in for it: a device that
|
||||
// diffs against its own idea of what is on screen will do nothing at
|
||||
// all when the screen has been corrupted by something else's output,
|
||||
// which is the case the player types CTRL-R for. It repaints what was
|
||||
// last rendered — C repainted curscr, not stdscr — so it neither
|
||||
// needs nor takes a window.
|
||||
Repaint()
|
||||
// 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.
|
||||
type cell struct {
|
||||
ch byte
|
||||
standout bool
|
||||
}
|
||||
|
||||
// Window is an in-memory curses window: a cell grid with a cursor and a
|
||||
// standout attribute.
|
||||
type Window struct {
|
||||
rows, cols int
|
||||
cells []cell
|
||||
cy, cx int
|
||||
standout bool
|
||||
}
|
||||
|
||||
// NewWindow returns a cleared window.
|
||||
func NewWindow(rows, cols int) *Window {
|
||||
w := &Window{rows: rows, cols: cols, cells: make([]cell, rows*cols)}
|
||||
w.Clear()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Move positions the cursor (curses move/wmove).
|
||||
func (w *Window) Move(y, x int) { w.cy, w.cx = y, x }
|
||||
|
||||
// GetYX reports the cursor position (curses getyx).
|
||||
func (w *Window) GetYX() (int, int) { return w.cy, w.cx }
|
||||
|
||||
// AddCh writes a character at the cursor and advances it (curses addch).
|
||||
func (w *Window) AddCh(ch byte) {
|
||||
if ch == '\n' {
|
||||
w.cy, w.cx = w.cy+1, 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
|
||||
return
|
||||
}
|
||||
|
||||
*w.at(w.cy, w.cx) = cell{ch: ch, standout: w.standout}
|
||||
if w.cx++; w.cx >= w.cols {
|
||||
w.cx = 0
|
||||
if w.cy < w.rows-1 {
|
||||
w.cy++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddStr writes a string at the cursor (curses addstr).
|
||||
func (w *Window) AddStr(s string) {
|
||||
for i := range len(s) {
|
||||
w.AddCh(s[i])
|
||||
}
|
||||
}
|
||||
|
||||
// MvAddCh moves then writes (curses mvaddch).
|
||||
func (w *Window) MvAddCh(y, x int, ch byte) {
|
||||
w.Move(y, x)
|
||||
w.AddCh(ch)
|
||||
}
|
||||
|
||||
// MvAddStr moves then writes (curses mvaddstr).
|
||||
func (w *Window) MvAddStr(y, x int, s string) {
|
||||
w.Move(y, x)
|
||||
w.AddStr(s)
|
||||
}
|
||||
|
||||
// Printwf writes formatted text at the cursor (curses printw).
|
||||
func (w *Window) Printwf(format string, a ...any) {
|
||||
w.AddStr(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// MvPrintwf moves then writes formatted text (curses mvprintw).
|
||||
func (w *Window) MvPrintwf(y, x int, format string, a ...any) {
|
||||
w.Move(y, x)
|
||||
w.Printwf(format, a...)
|
||||
}
|
||||
|
||||
// Inch returns the character under the cursor (curses inch, sans
|
||||
// attributes — the C code always strips them with CCHAR).
|
||||
func (w *Window) Inch() byte {
|
||||
if w.cy < 0 || w.cy >= w.rows || w.cx < 0 || w.cx >= w.cols {
|
||||
return ' '
|
||||
}
|
||||
|
||||
return w.at(w.cy, w.cx).ch
|
||||
}
|
||||
|
||||
// MvInch moves then reads (curses mvinch).
|
||||
func (w *Window) MvInch(y, x int) byte {
|
||||
w.Move(y, x)
|
||||
|
||||
return w.Inch()
|
||||
}
|
||||
|
||||
// Standout sets or clears the standout attribute for subsequent writes
|
||||
// (curses standout/standend).
|
||||
func (w *Window) Standout(on bool) { w.standout = on }
|
||||
|
||||
// Clear blanks the window and homes the cursor (curses clear/wclear).
|
||||
func (w *Window) Clear() {
|
||||
for i := range w.cells {
|
||||
w.cells[i] = cell{ch: ' '}
|
||||
}
|
||||
|
||||
w.cy, w.cx = 0, 0
|
||||
}
|
||||
|
||||
// Clrtoeol blanks from the cursor to the end of the line (curses clrtoeol).
|
||||
func (w *Window) Clrtoeol() {
|
||||
if w.cy < 0 || w.cy >= w.rows {
|
||||
return
|
||||
}
|
||||
|
||||
for x := w.cx; x < w.cols; x++ {
|
||||
*w.at(w.cy, x) = cell{ch: ' '}
|
||||
}
|
||||
}
|
||||
|
||||
// CopyFrom copies another window's contents (curses overwrite).
|
||||
func (w *Window) CopyFrom(src *Window) {
|
||||
copy(w.cells, src.cells)
|
||||
}
|
||||
|
||||
// Size reports the window dimensions as rows, columns.
|
||||
func (w *Window) Size() (int, 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) (byte, bool) {
|
||||
c := w.at(y, x)
|
||||
|
||||
return c.ch, c.standout
|
||||
}
|
||||
|
||||
// Contents dumps the window characters row-major (the save file keeps the
|
||||
// visible map, as the C game saved the curses screen).
|
||||
func (w *Window) Contents() []byte {
|
||||
out := make([]byte, len(w.cells))
|
||||
for i, c := range w.cells {
|
||||
out[i] = c.ch
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// SetContents restores a Contents dump.
|
||||
func (w *Window) SetContents(data []byte) {
|
||||
for i := range w.cells {
|
||||
if i < len(data) {
|
||||
w.cells[i] = cell{ch: data[i]}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Line returns row y as a trimmed string; used by tests and the death/
|
||||
// victory screens.
|
||||
func (w *Window) Line(y int) string {
|
||||
buf := make([]byte, w.cols)
|
||||
for x := range w.cols {
|
||||
buf[x] = w.at(y, x).ch
|
||||
}
|
||||
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// at addresses the cell at (y, x) in the backing array.
|
||||
func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] }
|
||||
|
||||
// Screen bundles the two windows the game draws on with the device that
|
||||
// shows them.
|
||||
type Screen struct {
|
||||
term Terminal
|
||||
Std *Window // stdscr: the dungeon view
|
||||
Hw *Window // hw: the scratch window for overlays
|
||||
}
|
||||
|
||||
// NewScreen builds the standard 24x80 game screen.
|
||||
func NewScreen(term Terminal) *Screen {
|
||||
return &Screen{
|
||||
term: term,
|
||||
Std: NewWindow(NumLines, NumCols),
|
||||
Hw: NewWindow(NumLines, NumCols),
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh pushes stdscr to the device (curses refresh).
|
||||
func (s *Screen) Refresh() {
|
||||
if s.term != nil {
|
||||
s.term.Render(s.Std)
|
||||
}
|
||||
}
|
||||
|
||||
// Repaint forces the device to redraw everything it is showing, if there
|
||||
// is a device (curses clearok(curscr, TRUE) + wrefresh(curscr)).
|
||||
func (s *Screen) Repaint() {
|
||||
if s.term != nil {
|
||||
s.term.Repaint()
|
||||
}
|
||||
}
|
||||
|
||||
// Fini restores the terminal device, if there is one (curses endwin).
|
||||
func (s *Screen) Fini() {
|
||||
if s.term != nil {
|
||||
s.term.Fini()
|
||||
}
|
||||
}
|
||||
|
||||
// Interrupt wakes a device read that is blocked waiting for a key, if
|
||||
// there is a device. Called from the signal goroutine; everything else on
|
||||
// Screen belongs to the game goroutine.
|
||||
func (s *Screen) Interrupt() {
|
||||
if s.term != nil {
|
||||
s.term.Interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshWin pushes an arbitrary window to the device (curses wrefresh).
|
||||
func (s *Screen) RefreshWin(w *Window) {
|
||||
if s.term != nil {
|
||||
s.term.Render(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Thin RogueGame wrappers so ported bodies keep their curses shape.
|
||||
|
||||
func (g *RogueGame) move(y, x int) { g.scr.Std.Move(y, x) }
|
||||
func (g *RogueGame) addch(ch byte) { g.scr.Std.AddCh(ch) }
|
||||
func (g *RogueGame) addstr(s string) { g.scr.Std.AddStr(s) }
|
||||
func (g *RogueGame) mvaddch(y, x int, c byte) { g.scr.Std.MvAddCh(y, x, c) }
|
||||
func (g *RogueGame) mvaddstr(y, x int, s string) {
|
||||
g.scr.Std.MvAddStr(y, x, s)
|
||||
}
|
||||
func (g *RogueGame) printw(f string, a ...any) { g.scr.Std.Printwf(f, a...) }
|
||||
func (g *RogueGame) inch() byte { return g.scr.Std.Inch() }
|
||||
func (g *RogueGame) mvinch(y, x int) byte { return g.scr.Std.MvInch(y, x) }
|
||||
func (g *RogueGame) standout() { g.scr.Std.Standout(true) }
|
||||
func (g *RogueGame) standend() { g.scr.Std.Standout(false) }
|
||||
func (g *RogueGame) clear() { g.scr.Std.Clear() }
|
||||
func (g *RogueGame) clrtoeol() { g.scr.Std.Clrtoeol() }
|
||||
func (g *RogueGame) refresh() { g.scr.Refresh() }
|
||||
func (g *RogueGame) repaint() { g.scr.Repaint() }
|
||||
383
game/scrolls.go
383
game/scrolls.go
@@ -1,383 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// scrolls.c — read a scroll and let it happen.
|
||||
|
||||
// readScroll reads a scroll from the pack and does the appropriate thing
|
||||
// (scrolls.c read_scroll).
|
||||
func (g *RogueGame) readScroll() {
|
||||
p := &g.Player
|
||||
|
||||
obj, ok := g.promptPackItem("read", KindScroll)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindScroll {
|
||||
if !g.Options.Terse {
|
||||
g.msg("there is nothing on it to read")
|
||||
} else {
|
||||
g.msg("nothing to read")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// Calculate the effect it has on the poor guy.
|
||||
if obj == p.CurWeapon {
|
||||
p.CurWeapon = nil
|
||||
}
|
||||
// Get rid of the thing
|
||||
g.leavePack(obj, false, false)
|
||||
|
||||
if h := g.data.readHandler(obj); h != nil {
|
||||
h(g, obj)
|
||||
}
|
||||
|
||||
g.look(true) // put the result of the scroll on the screen
|
||||
g.status()
|
||||
|
||||
// A malformed scroll has no lore entry to name (see quaff).
|
||||
if obj.hasValidWhich() {
|
||||
g.callIt(&g.Items.Scrolls[obj.Which])
|
||||
}
|
||||
}
|
||||
|
||||
// The per-scroll effect handlers, dispatched through
|
||||
// gameData.readHandlers. Each is one case of the C read_scroll switch.
|
||||
|
||||
func (g *RogueGame) readMonsterConfusion(*Object) {
|
||||
// Scroll of monster confusion. Give him that power.
|
||||
g.Player.Flags.Set(CanConfuse)
|
||||
g.msg("your hands begin to glow %s", g.pickColor("red"))
|
||||
}
|
||||
|
||||
func (g *RogueGame) readEnchantArmor(*Object) {
|
||||
p := &g.Player
|
||||
if p.CurArmor != nil {
|
||||
p.CurArmor.ArmorClass--
|
||||
p.CurArmor.Flags.Clear(Cursed)
|
||||
g.msg("your armor glows %s for a moment", g.pickColor("silver"))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) readHoldMonster(*Object) {
|
||||
// Hold monster scroll. Stop all monsters within two spaces from
|
||||
// chasing after the hero.
|
||||
held := g.holdMonstersNear()
|
||||
|
||||
if held > 0 {
|
||||
g.addmsgf("the monster")
|
||||
|
||||
if held > 1 {
|
||||
g.addmsgf("s around you")
|
||||
}
|
||||
|
||||
g.addmsgf(" freeze")
|
||||
|
||||
if held == 1 {
|
||||
g.addmsgf("s")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
g.Items.Scrolls[ScrollHoldMonster].Know = true
|
||||
} else {
|
||||
g.msg("you feel a strange sense of loss")
|
||||
}
|
||||
}
|
||||
|
||||
// holdMonstersNear freezes every awake monster within two spaces of the
|
||||
// hero and reports how many froze (the scan of the C S_HOLD case).
|
||||
func (g *RogueGame) holdMonstersNear() int {
|
||||
p := &g.Player
|
||||
held := 0
|
||||
|
||||
for x := p.Pos.X - 2; x <= p.Pos.X+2; x++ {
|
||||
if x < 0 || x >= NumCols {
|
||||
continue
|
||||
}
|
||||
|
||||
for y := p.Pos.Y - 2; y <= p.Pos.Y+2; y++ {
|
||||
if y < 0 || y > NumLines-1 {
|
||||
continue
|
||||
}
|
||||
|
||||
if mp := g.Level.MonsterAt(y, x); mp != nil && mp.On(Awake) {
|
||||
mp.Flags.Clear(Awake)
|
||||
mp.Flags.Set(Held)
|
||||
|
||||
held++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return held
|
||||
}
|
||||
|
||||
func (g *RogueGame) readSleep(*Object) {
|
||||
// Scroll which makes you fall asleep
|
||||
g.Items.Scrolls[ScrollSleep].Know = true
|
||||
g.NoCommand += g.rnd(g.spread(5)) + 4 // SLEEPTIME
|
||||
|
||||
g.Player.Flags.Clear(Awake)
|
||||
g.msg("you fall asleep")
|
||||
}
|
||||
|
||||
func (g *RogueGame) readCreateMonster(*Object) {
|
||||
// Create a monster: first look in a circle around him, next try
|
||||
// his room, otherwise give up
|
||||
mp, ok := g.createMonsterSpot()
|
||||
if !ok {
|
||||
g.msg("you hear a faint cry of anguish in the distance")
|
||||
} else {
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, g.randMonster(false), mp)
|
||||
}
|
||||
}
|
||||
|
||||
// createMonsterSpot reservoir-samples a legal spot around the hero for a
|
||||
// created monster; ok is false when every neighbor is blocked (the scan
|
||||
// of the C S_CREATE case).
|
||||
func (g *RogueGame) createMonsterSpot() (Coord, bool) {
|
||||
p := &g.Player
|
||||
i := 0
|
||||
|
||||
var mp Coord
|
||||
|
||||
for y := p.Pos.Y - 1; y <= p.Pos.Y+1; y++ {
|
||||
for x := p.Pos.X - 1; x <= p.Pos.X+1; x++ {
|
||||
// Don't put a monster on top of the player.
|
||||
if y == p.Pos.Y && x == p.Pos.X {
|
||||
continue
|
||||
}
|
||||
// Or anything else nasty
|
||||
if ch := g.Level.VisibleChar(y, x); stepOk(ch) {
|
||||
if ch == Scroll {
|
||||
if fo := g.Level.ObjectAt(y, x); fo != nil &&
|
||||
fo.ScrollKind() == ScrollScareMonster {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if i++; g.rnd(i) == 0 {
|
||||
mp = Coord{Y: y, X: x}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mp, i != 0
|
||||
}
|
||||
|
||||
func (g *RogueGame) readIdentify(obj *Object) {
|
||||
// Identify, let him figure something out. idType is shorter than the
|
||||
// scroll table keying it (it stops after the last identify scroll),
|
||||
// so the filter lookup carries its own bound. That bound is not
|
||||
// reachable today: readHandlers registers readIdentify only for the
|
||||
// identify scrolls, all of which sit inside idType. It is kept as
|
||||
// defense-in-depth against a future table resize.
|
||||
g.Items.Scrolls[obj.Which].Know = true
|
||||
g.msg("this scroll is an %s scroll", g.Items.Scrolls[obj.Which].Name)
|
||||
g.whatis(true, g.data.identifyType(obj.ScrollKind()))
|
||||
}
|
||||
|
||||
func (g *RogueGame) readMagicMapping(*Object) {
|
||||
// Scroll of magic mapping.
|
||||
g.Items.Scrolls[ScrollMagicMapping].Know = true
|
||||
g.msg("oh, now this scroll has a map on it")
|
||||
// take all the things we want to keep hidden out of the window
|
||||
for y := 1; y < NumLines-1; y++ {
|
||||
for x := range NumCols {
|
||||
g.revealSpot(y, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// revealSpot uncovers one map cell for magic mapping and draws it (the
|
||||
// loop body of the C SCR_MAP case).
|
||||
func (g *RogueGame) revealSpot(y, x int) {
|
||||
pp := g.Level.At(y, x)
|
||||
|
||||
ch := revealChar(pp)
|
||||
if ch != ' ' {
|
||||
if tp := pp.Monst; tp != nil {
|
||||
tp.OldCh = ch
|
||||
if !g.Player.On(SenseMonsters) {
|
||||
g.mvaddch(y, x, ch)
|
||||
}
|
||||
} else {
|
||||
g.mvaddch(y, x, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// revealChar decides what magic mapping shows at a cell, making secret
|
||||
// doors, hidden passages, and hidden traps real as a side effect; ' '
|
||||
// means show nothing (the switch of the C SCR_MAP loop).
|
||||
func revealChar(pp *Place) byte {
|
||||
ch := pp.Ch
|
||||
pass := false
|
||||
|
||||
switch ch {
|
||||
case Door, Stairs:
|
||||
case '-', '|':
|
||||
ch = revealWall(pp)
|
||||
case ' ':
|
||||
pass = revealSolid(pp)
|
||||
case Passage:
|
||||
pass = true
|
||||
case Floor:
|
||||
ch = revealFloor(pp)
|
||||
default:
|
||||
if pp.Flags.Has(FPassage) {
|
||||
pass = true
|
||||
} else {
|
||||
ch = ' '
|
||||
}
|
||||
}
|
||||
|
||||
if pass {
|
||||
if !pp.Flags.Has(FReal) {
|
||||
pp.Ch = Passage
|
||||
}
|
||||
|
||||
pp.Flags.Set(FSeen | FReal)
|
||||
|
||||
ch = Passage
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// revealWall handles a wall cell for magic mapping: a secret door
|
||||
// becomes a real door (the '-'/'|' arm).
|
||||
func revealWall(pp *Place) byte {
|
||||
if !pp.Flags.Has(FReal) {
|
||||
pp.Ch = Door
|
||||
pp.Flags.Set(FReal)
|
||||
|
||||
return Door
|
||||
}
|
||||
|
||||
return pp.Ch
|
||||
}
|
||||
|
||||
// revealSolid handles a blank cell for magic mapping, reporting whether
|
||||
// it is passage: hidden passages become real; hidden things in walls
|
||||
// stay hidden (the ' ' arm).
|
||||
func revealSolid(pp *Place) bool {
|
||||
if pp.Flags.Has(FReal) {
|
||||
return pp.Flags.Has(FPassage)
|
||||
}
|
||||
|
||||
pp.Flags.Set(FReal)
|
||||
pp.Ch = Passage
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// revealFloor handles a floor cell for magic mapping: a hidden trap
|
||||
// becomes a real, seen trap; real floor shows nothing (the FLOOR arm).
|
||||
func revealFloor(pp *Place) byte {
|
||||
if pp.Flags.Has(FReal) {
|
||||
return ' '
|
||||
}
|
||||
|
||||
pp.Ch = Trap
|
||||
pp.Flags.Set(FSeen | FReal)
|
||||
|
||||
return Trap
|
||||
}
|
||||
|
||||
func (g *RogueGame) readFoodDetection(*Object) {
|
||||
// Food detection
|
||||
found := false
|
||||
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
for _, fo := range g.Level.Objects {
|
||||
if fo.Kind == KindFood {
|
||||
found = true
|
||||
|
||||
g.scr.Hw.MvAddCh(fo.Pos.Y, fo.Pos.X, Food)
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
g.Items.Scrolls[ScrollFoodDetection].Know = true
|
||||
g.showWin("Your nose tingles and you smell food.--More--")
|
||||
} else {
|
||||
g.msg("your nose tingles")
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) readTeleportation(*Object) {
|
||||
// Scroll of teleportation: make him disappear and reappear
|
||||
p := &g.Player
|
||||
curRoom := p.Room
|
||||
|
||||
g.teleport()
|
||||
|
||||
if curRoom != p.Room {
|
||||
g.Items.Scrolls[ScrollTeleportation].Know = true
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) readEnchantWeapon(*Object) {
|
||||
p := &g.Player
|
||||
if p.CurWeapon == nil || p.CurWeapon.Kind != KindWeapon {
|
||||
g.msg("you feel a strange sense of loss")
|
||||
} else {
|
||||
p.CurWeapon.Flags.Clear(Cursed)
|
||||
|
||||
if g.rnd(2) == 0 {
|
||||
p.CurWeapon.HPlus++
|
||||
} else {
|
||||
p.CurWeapon.DPlus++
|
||||
}
|
||||
|
||||
g.msg("your %s glows %s for a moment",
|
||||
g.Items.Weapons[p.CurWeapon.Which].Name, g.pickColor("blue"))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *RogueGame) readScareMonster(*Object) {
|
||||
// Reading it is a mistake and produces laughter at her poor boo
|
||||
// boo.
|
||||
g.msg("you hear maniacal laughter in the distance")
|
||||
}
|
||||
|
||||
func (g *RogueGame) readRemoveCurse(*Object) {
|
||||
p := &g.Player
|
||||
|
||||
uncurse(p.CurArmor)
|
||||
uncurse(p.CurWeapon)
|
||||
uncurse(p.CurRing[Left])
|
||||
uncurse(p.CurRing[Right])
|
||||
g.msg("%s", g.chooseStr("you feel in touch with the Universal Onenes",
|
||||
"you feel as if somebody is watching over you"))
|
||||
}
|
||||
|
||||
func (g *RogueGame) readAggravateMonsters(*Object) {
|
||||
// This scroll aggravates all the monsters on the current level
|
||||
// and sets them running towards the hero
|
||||
g.aggravate()
|
||||
g.msg("you hear a high pitched humming noise")
|
||||
}
|
||||
|
||||
func (g *RogueGame) readProtectArmor(*Object) {
|
||||
p := &g.Player
|
||||
if p.CurArmor != nil {
|
||||
p.CurArmor.Flags.Set(Protected)
|
||||
g.msg("your armor is covered by a shimmering %s shield",
|
||||
g.pickColor("gold"))
|
||||
} else {
|
||||
g.msg("you feel a strange sense of loss")
|
||||
}
|
||||
}
|
||||
|
||||
// uncurse uncurses an item (scrolls.c uncurse).
|
||||
func uncurse(obj *Object) {
|
||||
if obj != nil {
|
||||
obj.Flags.Clear(Cursed)
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// dumpItemTables formats a game's per-seed item appearance tables in the
|
||||
// same layout the instrumented C reference prints: the potion colors,
|
||||
// scroll names, ring stones, and wand/staff materials, each generated by
|
||||
// consuming the RNG in a fixed order during New().
|
||||
func dumpItemTables(seed int32, g *RogueGame) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "SEED %d\n", seed)
|
||||
|
||||
fmt.Fprintln(&b, "POTIONS")
|
||||
|
||||
for _, c := range g.Items.PotColors {
|
||||
fmt.Fprintln(&b, c)
|
||||
}
|
||||
|
||||
fmt.Fprintln(&b, "SCROLLS")
|
||||
|
||||
for _, s := range g.Items.ScrNames {
|
||||
fmt.Fprintln(&b, s)
|
||||
}
|
||||
|
||||
fmt.Fprintln(&b, "RINGS")
|
||||
|
||||
for _, s := range g.Items.RingStones {
|
||||
fmt.Fprintln(&b, s)
|
||||
}
|
||||
|
||||
fmt.Fprintln(&b, "STICKS")
|
||||
|
||||
for i := range g.Items.WandType {
|
||||
fmt.Fprintf(&b, "%s %s\n", g.Items.WandType[i], g.Items.WandMade[i])
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// TestSeedCompatItemTables proves the port's seed-compatibility claim: for
|
||||
// the same seed, the Go game generates the exact per-seed item appearance
|
||||
// tables as the C reference on modern-rogue. That requires the LCG and its
|
||||
// consumption order through the whole init sequence (init_probs →
|
||||
// init_player → init_names → init_colors → init_stones → init_materials) to
|
||||
// match C byte for byte. The golden is captured from an instrumented build
|
||||
// of the C game (testdata/README.md).
|
||||
func TestSeedCompatItemTables(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
golden, err := os.ReadFile("testdata/item_tables.golden")
|
||||
if err != nil {
|
||||
t.Fatalf("read golden: %v", err)
|
||||
}
|
||||
|
||||
// These must match the seeds the golden was generated from
|
||||
// (testdata/README.md).
|
||||
seeds := []int32{1, 42, 12345, 99999}
|
||||
|
||||
var got strings.Builder
|
||||
|
||||
for _, seed := range seeds {
|
||||
g := New(Params{Seed: seed, Wizard: true})
|
||||
got.WriteString(dumpItemTables(seed, g))
|
||||
}
|
||||
|
||||
if got.String() != string(golden) {
|
||||
t.Errorf("Go item tables diverge from the C reference at %s",
|
||||
firstDiff(string(golden), got.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// firstDiff returns a description of the first line where want and got
|
||||
// differ, for a readable failure.
|
||||
func firstDiff(want, got string) string {
|
||||
wl := strings.Split(want, "\n")
|
||||
gl := strings.Split(got, "\n")
|
||||
|
||||
for i := 0; i < len(wl) || i < len(gl); i++ {
|
||||
w, g := "", ""
|
||||
if i < len(wl) {
|
||||
w = wl[i]
|
||||
}
|
||||
|
||||
if i < len(gl) {
|
||||
g = gl[i]
|
||||
}
|
||||
|
||||
if w != g {
|
||||
return fmt.Sprintf("line %d: C=%q Go=%q", i+1, w, g)
|
||||
}
|
||||
}
|
||||
|
||||
return "no line difference (trailing content?)"
|
||||
}
|
||||
558
game/sticks.go
558
game/sticks.go
@@ -1,558 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// sticks.c — zap wands and staffs.
|
||||
|
||||
// The two ws_type strings a stick can be made as.
|
||||
const (
|
||||
wandName = "wand"
|
||||
staffName = "staff"
|
||||
)
|
||||
|
||||
// doZap performs a zap with a wand (sticks.c do_zap).
|
||||
func (g *RogueGame) doZap() {
|
||||
obj, ok := g.promptPackItem("zap with", KindWand)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind != KindWand {
|
||||
g.After = false
|
||||
g.msg("you can't zap with that!")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Charges == 0 {
|
||||
g.msg("nothing happens")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// C's switch has a case for every one of the 14 WS_ kinds, so its
|
||||
// closing "otherwise: msg(...)" arm is reachable only for an o_which
|
||||
// outside the table — the malformed objects hasValidWhich screens
|
||||
// for, which is why no handler and a legal Which can only mean WS_NOP.
|
||||
//
|
||||
// The message is under #ifdef MASTER, not under a wizard test: C
|
||||
// printed it for every player of a MASTER build, which is the build
|
||||
// this port is (see the '+' command, issue #11). Do not gate it on
|
||||
// g.Wizard.
|
||||
//
|
||||
// WS_NOP is a case of its own ("when WS_NOP: break;"): the wand that
|
||||
// deliberately does nothing says nothing either. All three arms fall
|
||||
// out of the switch into o_charges--, so even the bizarre schtick
|
||||
// costs a charge.
|
||||
h := g.data.zapHandler(obj)
|
||||
|
||||
switch {
|
||||
case h != nil:
|
||||
if !h(g, obj) {
|
||||
return // the zap aborted; no charge is used
|
||||
}
|
||||
case obj.hasValidWhich(): // WS_NOP
|
||||
default:
|
||||
g.msg("what a bizarre schtick!")
|
||||
}
|
||||
|
||||
obj.Charges--
|
||||
}
|
||||
|
||||
// zapRayMonster walks the zap ray from the hero to the first blocking
|
||||
// spot and returns the monster standing there, if any (the shared
|
||||
// preamble of the C monster-affecting zap cases).
|
||||
func (g *RogueGame) zapRayMonster() *Monster {
|
||||
p := &g.Player
|
||||
|
||||
y := p.Pos.Y
|
||||
|
||||
x := p.Pos.X
|
||||
for stepOk(g.Level.VisibleChar(y, x)) {
|
||||
y += g.Delta.Y
|
||||
x += g.Delta.X
|
||||
}
|
||||
|
||||
return g.Level.MonsterAt(y, x)
|
||||
}
|
||||
|
||||
// zapVictim is zapRayMonster plus the flytrap release the C code does
|
||||
// before the invisibility-family effects.
|
||||
func (g *RogueGame) zapVictim() *Monster {
|
||||
tp := g.zapRayMonster()
|
||||
if tp != nil && tp.Type == 'F' {
|
||||
g.Player.Flags.Clear(Held)
|
||||
}
|
||||
|
||||
return tp
|
||||
}
|
||||
|
||||
// The per-wand effect handlers, dispatched through gameData.zapHandlers.
|
||||
// Each is one case of the C do_zap switch; returning false aborts the
|
||||
// zap without using a charge.
|
||||
|
||||
func (g *RogueGame) zapLight(*Object) bool {
|
||||
// Reddy Kilowatt wand. Light up the room
|
||||
p := &g.Player
|
||||
|
||||
g.Items.Sticks[WandLight].Know = true
|
||||
if p.Room.Flags.Has(Gone) {
|
||||
g.msg("the corridor glows and then fades")
|
||||
} else {
|
||||
p.Room.Flags.Clear(Dark)
|
||||
// Light the room and put the player back up
|
||||
g.enterRoom(p.Pos)
|
||||
g.addmsgf("the room is lit")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" by a shimmering %s light", g.pickColor("blue"))
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapDrainLife(*Object) bool {
|
||||
// take away 1/2 of hero's hit points, then take it away evenly
|
||||
// from the monsters in the room (or next to hero if he is in a
|
||||
// passage)
|
||||
if g.Player.Stats.HP < 2 {
|
||||
g.msg("you are too weak to use it")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
g.drain()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapInvisibility(*Object) bool {
|
||||
if tp := g.zapVictim(); tp != nil {
|
||||
tp.Flags.Set(Invisible)
|
||||
|
||||
if g.canSee(tp.Pos.Y, tp.Pos.X) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.OldCh)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapPolymorph(*Object) bool {
|
||||
tp := g.zapVictim()
|
||||
if tp == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
y, x := tp.Pos.Y, tp.Pos.X
|
||||
|
||||
pp := tp.Pack
|
||||
g.Level.RemoveMonster(tp)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, g.Level.Char(y, x))
|
||||
}
|
||||
|
||||
oldch := tp.OldCh
|
||||
g.Delta.Y = y
|
||||
g.Delta.X = x
|
||||
monster := g.randomMonsterLetter()
|
||||
g.newMonster(tp, monster, g.Delta)
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(y, x, monster)
|
||||
}
|
||||
|
||||
tp.OldCh = oldch
|
||||
|
||||
tp.Pack = pp
|
||||
if g.seeMonst(tp) {
|
||||
g.Items.Sticks[WandPolymorph].Know = true
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapCancellation(*Object) bool {
|
||||
if tp := g.zapVictim(); tp != nil {
|
||||
tp.Flags.Set(Cancelled)
|
||||
tp.Flags.Clear(Invisible | CanConfuse)
|
||||
|
||||
tp.Disguise = tp.Type
|
||||
if g.seeMonst(tp) {
|
||||
g.mvaddch(tp.Pos.Y, tp.Pos.X, tp.Disguise)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapTeleport(obj *Object) bool {
|
||||
p := &g.Player
|
||||
|
||||
tp := g.zapVictim()
|
||||
if tp == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
var newPos Coord
|
||||
|
||||
if obj.WandKind() == WandTeleportAway {
|
||||
for {
|
||||
newPos, _ = g.findFloor(true)
|
||||
if newPos != p.Pos {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newPos.Y = p.Pos.Y + g.Delta.Y
|
||||
newPos.X = p.Pos.X + g.Delta.X
|
||||
}
|
||||
|
||||
tp.Dest = &p.Pos
|
||||
tp.Flags.Set(Awake)
|
||||
g.relocate(tp, newPos)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapMagicMissile(*Object) bool {
|
||||
p := &g.Player
|
||||
|
||||
g.Items.Sticks[WandMagicMissile].Know = true
|
||||
bolt := newObject()
|
||||
bolt.Kind = KindGold // C set o_type='*': draws a '*' and is not a weapon
|
||||
bolt.HurlDmg = dice("1x4")
|
||||
bolt.HPlus = 100
|
||||
bolt.DPlus = 1
|
||||
|
||||
bolt.Flags = Missile
|
||||
if p.CurWeapon != nil {
|
||||
bolt.Launch = WeaponKind(p.CurWeapon.Which)
|
||||
}
|
||||
|
||||
g.doMotion(bolt, g.Delta.Y, g.Delta.X)
|
||||
|
||||
if tp := g.Level.MonsterAt(bolt.Pos.Y, bolt.Pos.X); tp != nil &&
|
||||
!g.saveThrow(VsMagic, &tp.Stats) {
|
||||
g.hitMonster(bolt.Pos, bolt)
|
||||
} else if g.Options.Terse {
|
||||
g.msg("missle vanishes") //nolint:misspell // C's spelling, kept faithfully
|
||||
} else {
|
||||
g.msg("the missle vanishes with a puff of smoke") //nolint:misspell // C's spelling
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapSpeed(obj *Object) bool {
|
||||
tp := g.zapRayMonster()
|
||||
if tp == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if obj.WandKind() == WandHasteMonster {
|
||||
hasteTarget(tp)
|
||||
} else {
|
||||
slowTarget(tp)
|
||||
}
|
||||
|
||||
g.Delta.Y = tp.Pos.Y
|
||||
g.Delta.X = tp.Pos.X
|
||||
g.runTo(g.Delta)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// hasteTarget cancels a slow or applies a haste (the WS_HASTE_M arm of
|
||||
// do_zap).
|
||||
func hasteTarget(tp *Monster) {
|
||||
if tp.On(Slowed) {
|
||||
tp.Flags.Clear(Slowed)
|
||||
} else {
|
||||
tp.Flags.Set(Hasted)
|
||||
}
|
||||
}
|
||||
|
||||
// slowTarget cancels a haste or applies a slow (the WS_SLOW_M arm of
|
||||
// do_zap).
|
||||
func slowTarget(tp *Monster) {
|
||||
if tp.On(Hasted) {
|
||||
tp.Flags.Clear(Hasted)
|
||||
} else {
|
||||
tp.Flags.Set(Slowed)
|
||||
}
|
||||
|
||||
tp.Turn = true
|
||||
}
|
||||
|
||||
func (g *RogueGame) zapBolt(obj *Object) bool {
|
||||
var name string
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.WandKind() {
|
||||
case WandLightning:
|
||||
name = "bolt"
|
||||
case WandFire:
|
||||
name = "flame"
|
||||
default:
|
||||
name = "ice"
|
||||
}
|
||||
|
||||
g.fireBolt(g.Player.Pos, &g.Delta, name)
|
||||
g.Items.Sticks[obj.Which].Know = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// drain does the drain-hit-points-from-player schtick (sticks.c drain).
|
||||
func (g *RogueGame) drain() {
|
||||
p := &g.Player
|
||||
// First count how many things we need to spread the hit points among
|
||||
var corp *Room
|
||||
if g.Level.Char(p.Pos.Y, p.Pos.X) == Door {
|
||||
corp = &g.Level.Passages[*g.Level.FlagsAt(p.Pos.Y, p.Pos.X)&FPassNum]
|
||||
}
|
||||
|
||||
inpass := p.Room.Flags.Has(Gone)
|
||||
|
||||
var drainee []*Monster
|
||||
|
||||
for _, mp := range g.Level.Monsters {
|
||||
if g.drainReaches(mp, corp, inpass) {
|
||||
drainee = append(drainee, mp)
|
||||
}
|
||||
}
|
||||
|
||||
cnt := len(drainee)
|
||||
if cnt == 0 {
|
||||
g.msg("you have a tingling feeling")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.Stats.HP /= 2
|
||||
cnt = p.Stats.HP / cnt
|
||||
// Now zot all of the monsters
|
||||
for _, mp := range drainee {
|
||||
if mp.Stats.HP -= cnt; mp.Stats.HP <= 0 {
|
||||
g.killed(mp, g.seeMonst(mp))
|
||||
} else {
|
||||
g.runTo(mp.Pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drainReaches reports whether the drain-life wand reaches this monster:
|
||||
// the hero's room, the passage behind the door he stands on, or — when
|
||||
// he is in a passage — a door of that same passage (the drainee
|
||||
// condition of sticks.c drain).
|
||||
func (g *RogueGame) drainReaches(mp *Monster, corp *Room, inpass bool) bool {
|
||||
p := &g.Player
|
||||
|
||||
return mp.Room == p.Room || mp.Room == corp ||
|
||||
(inpass && g.Level.Char(mp.Pos.Y, mp.Pos.X) == Door &&
|
||||
&g.Level.Passages[*g.Level.FlagsAt(mp.Pos.Y, mp.Pos.X)&FPassNum] == p.Room)
|
||||
}
|
||||
|
||||
// fireBolt fires a bolt in a given direction from a specific starting
|
||||
// place (sticks.c fire_bolt).
|
||||
func (g *RogueGame) fireBolt(start Coord, dir *Coord, name string) {
|
||||
p := &g.Player
|
||||
fromHero := start == p.Pos
|
||||
|
||||
bolt := newObject()
|
||||
bolt.Kind = KindWeapon
|
||||
bolt.Which = int(WeaponFlame)
|
||||
bolt.HurlDmg = dice("6x6")
|
||||
bolt.HPlus = 100
|
||||
bolt.DPlus = 0
|
||||
g.Items.Weapons[WeaponFlame].Name = name
|
||||
|
||||
dirch := boltDirChar(*dir)
|
||||
pos := start
|
||||
hitHero := !fromHero
|
||||
used := false
|
||||
changed := false
|
||||
|
||||
var spotpos []Coord
|
||||
for len(spotpos) < BoltLength && !used {
|
||||
pos.Y += dir.Y
|
||||
pos.X += dir.X
|
||||
spotpos = append(spotpos, pos)
|
||||
|
||||
ch := g.Level.VisibleChar(pos.Y, pos.X)
|
||||
if boltBounces(ch, p.Pos, pos) {
|
||||
if !changed {
|
||||
hitHero = !hitHero
|
||||
}
|
||||
|
||||
changed = false
|
||||
dir.Y = -dir.Y
|
||||
dir.X = -dir.X
|
||||
spotpos = spotpos[:len(spotpos)-1]
|
||||
|
||||
g.msg("the %s bounces", name)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if tp := g.Level.MonsterAt(pos.Y, pos.X); !hitHero && tp != nil {
|
||||
hitHero = true
|
||||
changed = !changed
|
||||
used = g.boltStrikesMonster(tp, bolt, pos, ch, name, fromHero)
|
||||
} else if hitHero && pos == p.Pos {
|
||||
hitHero = false
|
||||
changed = !changed
|
||||
used = g.boltStrikesHero(start, name, fromHero)
|
||||
}
|
||||
|
||||
g.mvaddch(pos.Y, pos.X, dirch)
|
||||
g.refresh()
|
||||
}
|
||||
// erase the bolt trail
|
||||
for _, c2 := range spotpos {
|
||||
g.mvaddch(c2.Y, c2.X, g.Level.Char(c2.Y, c2.X))
|
||||
}
|
||||
}
|
||||
|
||||
// boltDirChar picks the character a traveling bolt is drawn with for its
|
||||
// direction (the dirch switch of sticks.c fire_bolt).
|
||||
func boltDirChar(dir Coord) byte {
|
||||
switch dir.Y + dir.X {
|
||||
case 0:
|
||||
return '/'
|
||||
case 1, -1:
|
||||
if dir.Y == 0 {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return '|'
|
||||
case 2, -2:
|
||||
return '\\'
|
||||
}
|
||||
|
||||
return 0 // unreachable for the eight legal directions, as in C
|
||||
}
|
||||
|
||||
// boltBounces reports whether a bolt bounces off this spot: walls, and
|
||||
// any door except the one the hero stands on (which would otherwise loop
|
||||
// infinitely, per the C comment in fire_bolt).
|
||||
func boltBounces(ch byte, heroPos, pos Coord) bool {
|
||||
switch ch {
|
||||
case Door:
|
||||
return heroPos != pos
|
||||
case '|', '-', ' ':
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// boltStrikesMonster resolves a bolt arriving on a monster's square (the
|
||||
// monster arm of the fire_bolt loop). It reports whether the bolt was
|
||||
// used up.
|
||||
func (g *RogueGame) boltStrikesMonster(
|
||||
tp *Monster, bolt *Object, pos Coord, ch byte, name string, fromHero bool,
|
||||
) bool {
|
||||
tp.OldCh = g.Level.Char(pos.Y, pos.X)
|
||||
if !g.saveThrow(VsMagic, &tp.Stats) {
|
||||
bolt.Pos = pos
|
||||
|
||||
if tp.Type == 'D' && name == "flame" {
|
||||
g.addmsgf("the flame bounces")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" off the dragon")
|
||||
}
|
||||
|
||||
g.endmsg()
|
||||
} else {
|
||||
g.hitMonster(pos, bolt)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if ch != 'M' || tp.Disguise == 'M' {
|
||||
if fromHero {
|
||||
g.runTo(pos)
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("%s misses", name)
|
||||
} else {
|
||||
g.msg("the %s whizzes past %s", name, g.setMname(tp))
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// boltStrikesHero resolves a bolt arriving on the hero (the hero arm of
|
||||
// the fire_bolt loop). It reports whether the bolt was used up.
|
||||
func (g *RogueGame) boltStrikesHero(start Coord, name string, fromHero bool) bool {
|
||||
p := &g.Player
|
||||
if g.save(VsMagic) {
|
||||
g.msg("the %s whizzes by you", name)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if p.Stats.HP -= g.roll(6, 6); p.Stats.HP <= 0 {
|
||||
if fromHero {
|
||||
g.death('b')
|
||||
} else {
|
||||
g.death(g.Level.MonsterAt(start.Y, start.X).Type)
|
||||
}
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
g.msg("the %s hits", name)
|
||||
} else {
|
||||
g.msg("you are hit by the %s", name)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// fixStick sets up a new wand or staff (sticks.c fix_stick).
|
||||
func (g *RogueGame) fixStick(cur *Object) {
|
||||
// ws_type[] is indexed by Which; a malformed one is treated as a wand,
|
||||
// which is the branch the C string compare would take against any
|
||||
// value that is not literally "staff". The charge switch below already
|
||||
// funnels everything but WandLight into its default arm.
|
||||
if cur.hasValidWhich() && g.Items.WandType[cur.Which] == staffName {
|
||||
cur.Damage = dice("2x3")
|
||||
} else {
|
||||
cur.Damage = dice("1x1")
|
||||
}
|
||||
|
||||
cur.HurlDmg = dice("1x1")
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch cur.WandKind() {
|
||||
case WandLight:
|
||||
cur.Charges = g.rnd(10) + 10
|
||||
default:
|
||||
cur.Charges = g.rnd(5) + 3
|
||||
}
|
||||
}
|
||||
|
||||
// chargeStr returns the charge-count suffix for an identified stick
|
||||
// (sticks.c charge_str).
|
||||
func chargeStr(g *RogueGame, obj *Object) string {
|
||||
if !obj.Flags.Has(Known) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if g.Options.Terse {
|
||||
return fmt.Sprintf(" [%d]", obj.Charges)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" [%d charges]", obj.Charges)
|
||||
}
|
||||
@@ -1,874 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The zap and bolt tests need a map they can reason about: real levels
|
||||
// put their rooms wherever rooms.c felt like, and sticks.c's geometry —
|
||||
// which square a bolt bounces off, which room a drain reaches — only
|
||||
// means anything against known walls, a known door, and a known passage
|
||||
// number. mkCarvedGame lays out two rooms joined by one corridor, using
|
||||
// the generator's own drawRoom so the wall characters (including the
|
||||
// '-' corners horiz() paints over vert()'s '|') are what a real level
|
||||
// would have:
|
||||
//
|
||||
// x: 1 20 40 59
|
||||
// y=1 -------------------- ------------------
|
||||
// |..................| |................|
|
||||
// y=4 |..................+########+................|
|
||||
// |..................| |................|
|
||||
// y=8 -------------------- ------------------
|
||||
const (
|
||||
carvedWidth = 20 // room width, both walls included
|
||||
carvedHeight = 8 // room height, both walls included
|
||||
roomAX = 1 // left wall of the west room
|
||||
roomBX = 40 // left wall of the east room
|
||||
carvedTopY = 1 // top wall of both rooms
|
||||
corridorY = 4 // row the corridor and doors run on
|
||||
doorAX = roomAX + carvedWidth - 1 // east wall of the west room
|
||||
carvedPass = 2 // passage number of the corridor
|
||||
)
|
||||
|
||||
// saveProofLvl makes save_throw(VS_MAGIC) succeed on every roll, so a
|
||||
// test can select the "it saved" arm without touching the RNG: C's
|
||||
// threshold is 14 + VS_MAGIC - lvl/2, which at level 40 is -3, and
|
||||
// roll(1,20) always clears that.
|
||||
const saveProofLvl = 40
|
||||
|
||||
// mkCarvedGame builds a game on the hand-carved level drawn above, with
|
||||
// the hero standing in the south-east corner of the west room — off
|
||||
// every row and column the bolt tests fire along.
|
||||
func mkCarvedGame(t *testing.T, seed int32) *RogueGame {
|
||||
t.Helper()
|
||||
|
||||
g := New(Params{Seed: seed, Term: &testTerm{}})
|
||||
for i := range g.Level.Places {
|
||||
g.Level.Places[i] = Place{Ch: ' ', Flags: FReal}
|
||||
}
|
||||
|
||||
for i, x := range [...]int{roomAX, roomBX} {
|
||||
rp := &g.Level.Rooms[i]
|
||||
*rp = Room{
|
||||
Pos: Coord{X: x, Y: carvedTopY},
|
||||
Max: Coord{X: carvedWidth, Y: carvedHeight},
|
||||
}
|
||||
g.drawRoom(rp)
|
||||
}
|
||||
|
||||
for i := 2; i < MaxRooms; i++ {
|
||||
g.Level.Rooms[i].Flags = Gone // rooms that are not there
|
||||
}
|
||||
|
||||
for x := doorAX + 1; x < roomBX; x++ {
|
||||
pp := g.Level.At(corridorY, x)
|
||||
pp.Ch = Passage
|
||||
pp.Flags = FReal | FPassage | carvedPass
|
||||
}
|
||||
// Doors carry the passage number in their low bits but not F_PASS,
|
||||
// exactly as passages.c numpass leaves them; roomin therefore reports
|
||||
// the room a door belongs to, and drain's corp lookup finds the
|
||||
// passage behind it.
|
||||
for _, x := range [...]int{doorAX, roomBX} {
|
||||
pp := g.Level.At(corridorY, x)
|
||||
pp.Ch = Door
|
||||
pp.Flags = FReal | carvedPass
|
||||
}
|
||||
|
||||
placeHero(g, Coord{X: roomAX + 17, Y: carvedTopY + 6})
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
// placeHero moves the hero and keeps proom, oldpos and oldrp in step,
|
||||
// the way move.c and misc.c look do; a --More-- prompt redraws through
|
||||
// look, which reads all three.
|
||||
func placeHero(g *RogueGame, pos Coord) {
|
||||
g.Player.Pos = pos
|
||||
g.Player.Room = g.roomIn(pos)
|
||||
g.Oldpos = pos
|
||||
g.Oldrp = g.Player.Room
|
||||
}
|
||||
|
||||
// putMonster drops a monster of the given letter on a carved-level spot.
|
||||
func putMonster(g *RogueGame, typ byte, pos Coord) *Monster {
|
||||
tp := &Monster{}
|
||||
g.newMonster(tp, typ, pos)
|
||||
|
||||
return tp
|
||||
}
|
||||
|
||||
// pinRng rewinds the generator to a state whose next draw is exactly
|
||||
// want, so tests can choose a save-throw outcome or a polymorph letter
|
||||
// without assuming anything about the generator itself: the wanted
|
||||
// value is found by running the real Rng, not by predicting it.
|
||||
func pinRng(t *testing.T, g *RogueGame, draw func(*Rng) int, want int) {
|
||||
t.Helper()
|
||||
|
||||
for s := int32(1); s < 100000; s++ {
|
||||
probe := Rng{Seed: s}
|
||||
if draw(&probe) == want {
|
||||
g.Rng.Seed = s
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("no seed found whose next draw is %d", want)
|
||||
}
|
||||
|
||||
// d20 is the save_throw draw (monsters.c save_throw: roll(1, 20)).
|
||||
func d20(r *Rng) int { return r.Roll(1, 20) }
|
||||
|
||||
// zapWand builds a wand of the given kind with charges to spare.
|
||||
func zapWand(kind WandKind) *Object {
|
||||
obj := newObject()
|
||||
obj.Kind = KindWand
|
||||
obj.Which = int(kind)
|
||||
obj.Charges = 5
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// TestZapLightLightsTheRoom covers the WS_LIGHT arm: the room loses
|
||||
// ISDARK, the wand identifies itself, and the message is C's two-part
|
||||
// one (sticks.c 71-89).
|
||||
func TestZapLightLightsTheRoom(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 11)
|
||||
g.Player.Room.Flags.Set(Dark)
|
||||
|
||||
g.zapLight(zapWand(WandLight))
|
||||
|
||||
if g.Player.Room.Flags.Has(Dark) {
|
||||
t.Error("the room is still dark after a wand of light")
|
||||
}
|
||||
|
||||
if !g.Items.Sticks[WandLight].Know {
|
||||
t.Error("the wand of light did not identify itself")
|
||||
}
|
||||
|
||||
const want = "the room is lit by a shimmering blue light"
|
||||
if g.Msgs.Huh != want {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapLightInPassageFades covers the ISGONE arm: a corridor is not a
|
||||
// room, so nothing is lit and the wand still becomes known.
|
||||
func TestZapLightInPassageFades(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 12)
|
||||
placeHero(g, Coord{X: doorAX + 3, Y: corridorY})
|
||||
g.Level.Rooms[0].Flags.Set(Dark)
|
||||
|
||||
g.zapLight(zapWand(WandLight))
|
||||
|
||||
if !g.Player.Room.Flags.Has(Gone) {
|
||||
t.Fatal("the hero is not in a passage; the test set-up is wrong")
|
||||
}
|
||||
|
||||
const want = "the corridor glows and then fades"
|
||||
if g.Msgs.Huh != want {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, want)
|
||||
}
|
||||
|
||||
if !g.Items.Sticks[WandLight].Know {
|
||||
t.Error("the wand of light did not identify itself in a corridor")
|
||||
}
|
||||
|
||||
if !g.Level.Rooms[0].Flags.Has(Dark) {
|
||||
t.Error("zapping in a corridor lit a room anyway")
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapDrainLifeTooWeakKeepsCharge covers C's early return: under two
|
||||
// hit points the zap is refused, and because C returns before the
|
||||
// switch falls out, o_charges-- never runs.
|
||||
func TestZapDrainLifeTooWeakKeepsCharge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 13)
|
||||
g.Player.Stats.HP = 1
|
||||
|
||||
wand := zapWand(WandDrainLife)
|
||||
ch := give(g, wand)
|
||||
setInput(t, g, ch)
|
||||
|
||||
g.doZap()
|
||||
|
||||
const want = "you are too weak to use it"
|
||||
if g.Msgs.Huh != want {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, want)
|
||||
}
|
||||
|
||||
if wand.Charges != 5 {
|
||||
t.Errorf("charges = %d, want 5: the refused zap must not cost one",
|
||||
wand.Charges)
|
||||
}
|
||||
|
||||
if g.Player.Stats.HP != 1 {
|
||||
t.Errorf("hit points = %d, want 1", g.Player.Stats.HP)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDrainSplitsHitPoints covers sticks.c drain: the hero loses half
|
||||
// his hit points and the drainees each lose that half divided by their
|
||||
// number — monsters out of reach lose nothing.
|
||||
func TestDrainSplitsHitPoints(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 14)
|
||||
placeHero(g, Coord{X: 5, Y: 3})
|
||||
g.Player.Stats.HP = 20
|
||||
|
||||
near := [2]*Monster{
|
||||
putMonster(g, 'Z', Coord{X: 7, Y: 3}),
|
||||
putMonster(g, 'Z', Coord{X: 9, Y: 5}),
|
||||
}
|
||||
far := putMonster(g, 'Z', Coord{X: roomBX + 5, Y: 3})
|
||||
|
||||
for _, tp := range []*Monster{near[0], near[1], far} {
|
||||
tp.Stats.HP = 100
|
||||
}
|
||||
|
||||
g.drain()
|
||||
|
||||
if g.Player.Stats.HP != 10 {
|
||||
t.Errorf("hero hit points = %d, want 10", g.Player.Stats.HP)
|
||||
}
|
||||
// 10 hit points spread over two drainees is 5 apiece.
|
||||
for i, tp := range near {
|
||||
if tp.Stats.HP != 95 {
|
||||
t.Errorf("drainee %d hit points = %d, want 95", i, tp.Stats.HP)
|
||||
}
|
||||
}
|
||||
|
||||
if far.Stats.HP != 100 {
|
||||
t.Errorf("the monster in the other room lost %d hit points",
|
||||
100-far.Stats.HP)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDrainWithNoTargetsCostsNothing covers the cnt == 0 arm, which
|
||||
// returns before pstats.s_hpt is halved.
|
||||
func TestDrainWithNoTargetsCostsNothing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 15)
|
||||
placeHero(g, Coord{X: 5, Y: 3})
|
||||
g.Player.Stats.HP = 20
|
||||
|
||||
g.drain()
|
||||
|
||||
const want = "you have a tingling feeling"
|
||||
if g.Msgs.Huh != want {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, want)
|
||||
}
|
||||
|
||||
if g.Player.Stats.HP != 20 {
|
||||
t.Errorf("hero hit points = %d, want 20: a drain that found nobody "+
|
||||
"returns before halving them", g.Player.Stats.HP)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDrainKillsWeakMonster covers the other arm of drain's zot loop: a
|
||||
// drainee whose share of the hit points finishes it is killed outright.
|
||||
func TestDrainKillsWeakMonster(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 29)
|
||||
placeHero(g, Coord{X: 5, Y: 3})
|
||||
g.Player.Stats.HP = 20
|
||||
|
||||
tp := putMonster(g, 'Z', Coord{X: 7, Y: 3})
|
||||
tp.Stats.HP = 3 // less than the ten points it is about to take
|
||||
|
||||
g.drain()
|
||||
|
||||
if len(g.Level.Monsters) != 0 {
|
||||
t.Error("the drained monster is still on the level")
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(7, 3) != nil {
|
||||
t.Error("the drained monster is still on the map")
|
||||
}
|
||||
|
||||
const want = "you have defeated the zombie"
|
||||
if g.Msgs.Huh != want {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapSpeedTogglesHasteAndSlow covers both WS_HASTE_M and WS_SLOW_M
|
||||
// in both directions: C cancels the opposite condition when it is
|
||||
// already on, and only otherwise applies its own.
|
||||
func TestZapSpeedTogglesHasteAndSlow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
kind WandKind
|
||||
start CreatureFlags
|
||||
wantHasted bool
|
||||
wantSlowed bool
|
||||
wantTurn bool
|
||||
}{
|
||||
{name: "haste a monster", kind: WandHasteMonster, wantHasted: true},
|
||||
{
|
||||
name: "haste cancels a slow",
|
||||
kind: WandHasteMonster,
|
||||
start: Slowed,
|
||||
},
|
||||
{
|
||||
name: "slow a monster",
|
||||
kind: WandSlowMonster,
|
||||
wantSlowed: true,
|
||||
wantTurn: true,
|
||||
},
|
||||
{
|
||||
name: "slow cancels a haste",
|
||||
kind: WandSlowMonster,
|
||||
start: Hasted,
|
||||
wantTurn: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 30)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
|
||||
tp := putMonster(g, 'Z', Coord{X: 8, Y: corridorY})
|
||||
tp.Flags.Clear(Hasted | Slowed)
|
||||
tp.Flags.Set(tt.start)
|
||||
tp.Turn = false // only the slow arm sets t_turn
|
||||
|
||||
g.zapSpeed(zapWand(tt.kind))
|
||||
|
||||
if tp.On(Hasted) != tt.wantHasted {
|
||||
t.Errorf("hasted = %v, want %v", tp.On(Hasted), tt.wantHasted)
|
||||
}
|
||||
|
||||
if tp.On(Slowed) != tt.wantSlowed {
|
||||
t.Errorf("slowed = %v, want %v", tp.On(Slowed), tt.wantSlowed)
|
||||
}
|
||||
|
||||
if tp.Turn != tt.wantTurn {
|
||||
t.Errorf("turn = %v, want %v", tp.Turn, tt.wantTurn)
|
||||
}
|
||||
|
||||
if !tp.On(Awake) {
|
||||
t.Error("the zapped monster was not set running")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDrainReaches pins the three-clause drainee test of sticks.c drain
|
||||
// one clause at a time: the hero's own room, the passage behind the door
|
||||
// he stands on (corp), and — only when he is in a passage — a door of
|
||||
// that same passage.
|
||||
func TestDrainReaches(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
heroPos Coord
|
||||
monstPos Coord
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "same room",
|
||||
heroPos: Coord{X: 5, Y: 3},
|
||||
monstPos: Coord{X: 9, Y: 6},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different room",
|
||||
heroPos: Coord{X: 5, Y: 3},
|
||||
monstPos: Coord{X: roomBX + 5, Y: 3},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "hero on a door reaches into that passage",
|
||||
heroPos: Coord{X: doorAX, Y: corridorY},
|
||||
monstPos: Coord{X: doorAX + 4, Y: corridorY},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "hero in the passage reaches its doors",
|
||||
heroPos: Coord{X: doorAX + 4, Y: corridorY},
|
||||
monstPos: Coord{X: roomBX, Y: corridorY},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "hero in the passage does not reach into a room",
|
||||
heroPos: Coord{X: doorAX + 4, Y: corridorY},
|
||||
monstPos: Coord{X: roomBX + 5, Y: 3},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 16)
|
||||
placeHero(g, tt.heroPos)
|
||||
tp := putMonster(g, 'Z', tt.monstPos)
|
||||
|
||||
var corp *Room
|
||||
if g.Level.Char(tt.heroPos.Y, tt.heroPos.X) == Door {
|
||||
corp = &g.Level.Passages[*g.Level.FlagsAt(
|
||||
tt.heroPos.Y, tt.heroPos.X)&FPassNum]
|
||||
}
|
||||
|
||||
inpass := g.Player.Room.Flags.Has(Gone)
|
||||
if got := g.drainReaches(tp, corp, inpass); got != tt.want {
|
||||
t.Errorf("drainReaches = %v, want %v (inpass=%v corp=%v)",
|
||||
got, tt.want, inpass, corp != nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapInvisibilityHidesMonster covers the WS_INVIS arm.
|
||||
func TestZapInvisibilityHidesMonster(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 17)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
|
||||
tp := putMonster(g, 'Z', Coord{X: 8, Y: corridorY})
|
||||
|
||||
g.zapInvisibility(zapWand(WandInvisibility))
|
||||
|
||||
if !tp.On(Invisible) {
|
||||
t.Error("the zapped monster is still visible")
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapVictimReleasesFlytrap covers the shared preamble of C's
|
||||
// invisibility family: the flytrap holding the hero lets go the moment
|
||||
// the ray reaches it, whichever of those wands was zapped.
|
||||
func TestZapVictimReleasesFlytrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 18)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
g.Player.Flags.Set(Held)
|
||||
|
||||
tp := putMonster(g, 'F', Coord{X: 6, Y: corridorY})
|
||||
|
||||
g.zapInvisibility(zapWand(WandInvisibility))
|
||||
|
||||
if g.Player.On(Held) {
|
||||
t.Error("the flytrap still holds the hero after the zap")
|
||||
}
|
||||
|
||||
if !tp.On(Invisible) {
|
||||
t.Error("the flytrap was not made invisible")
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapPolymorphReplacesMonster covers the WS_POLYMORPH arm and its
|
||||
// detach/re-attach dance: the creature keeps its identity (the same
|
||||
// THING, its pack, and the character it is standing on) but becomes a
|
||||
// different monster, listed once and standing where it stood.
|
||||
func TestZapPolymorphReplacesMonster(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 19)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
|
||||
pos := Coord{X: 8, Y: corridorY}
|
||||
tp := putMonster(g, 'K', pos)
|
||||
loot := newObject()
|
||||
loot.Kind = KindPotion
|
||||
tp.Pack = []*Object{loot}
|
||||
tp.OldCh = Stairs // it is standing on the staircase
|
||||
|
||||
const want = 'T'
|
||||
|
||||
pinRng(t, g, func(r *Rng) int { return r.Rnd(26) }, int(want-'A'))
|
||||
|
||||
g.zapPolymorph(zapWand(WandPolymorph))
|
||||
|
||||
if tp.Type != want || tp.Disguise != want {
|
||||
t.Errorf("monster is %q/%q after polymorph, want %q",
|
||||
tp.Type, tp.Disguise, want)
|
||||
}
|
||||
|
||||
if tp.Stats.Lvl != g.Monsters[want-'A'].Stats.Lvl {
|
||||
t.Errorf("level = %d, want the troll's %d: new_monster did not "+
|
||||
"re-roll the stats", tp.Stats.Lvl, g.Monsters[want-'A'].Stats.Lvl)
|
||||
}
|
||||
|
||||
if len(tp.Pack) != 1 || tp.Pack[0] != loot {
|
||||
t.Error("polymorph lost the monster's pack")
|
||||
}
|
||||
|
||||
if tp.OldCh != Stairs {
|
||||
t.Errorf("under-character = %q, want %q", tp.OldCh, Stairs)
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(pos.Y, pos.X) != tp || tp.Pos != pos {
|
||||
t.Error("the polymorphed monster is not where it stood")
|
||||
}
|
||||
|
||||
if n := len(g.Level.Monsters); n != 1 {
|
||||
t.Errorf("monster list holds %d entries, want 1: detach and "+
|
||||
"new_monster's attach must balance", n)
|
||||
}
|
||||
|
||||
if !g.Items.Sticks[WandPolymorph].Know {
|
||||
t.Error("a polymorph the hero watched did not identify the wand")
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapPolymorphClobbersDelta pins a C quirk the port keeps: do_zap
|
||||
// reuses the global delta as scratch for new_monster's coordinate, so
|
||||
// the zap direction is gone by the time the arm returns.
|
||||
func TestZapPolymorphClobbersDelta(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 20)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
|
||||
pos := Coord{X: 8, Y: corridorY}
|
||||
putMonster(g, 'K', pos)
|
||||
|
||||
g.zapPolymorph(zapWand(WandPolymorph))
|
||||
|
||||
if g.Delta != pos {
|
||||
t.Errorf("delta = %v after polymorph, want the victim's %v",
|
||||
g.Delta, pos)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapCancellationClearsSpecials covers the WS_CANCEL arm. CANHUH is
|
||||
// set on the player and never on a monster in C (only scrolls.c sets
|
||||
// it), so the test puts it on by hand: the clear is written to take both
|
||||
// bits and the port must keep doing so.
|
||||
func TestZapCancellationClearsSpecials(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 21)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
|
||||
tp := putMonster(g, 'M', Coord{X: 8, Y: corridorY})
|
||||
tp.Flags.Set(Invisible | CanConfuse)
|
||||
|
||||
g.zapCancellation(zapWand(WandCancellation))
|
||||
|
||||
if !tp.On(Cancelled) {
|
||||
t.Error("the monster was not cancelled")
|
||||
}
|
||||
|
||||
if tp.On(Invisible) {
|
||||
t.Error("cancellation left the monster invisible")
|
||||
}
|
||||
|
||||
if tp.On(CanConfuse) {
|
||||
t.Error("cancellation left the monster able to confuse")
|
||||
}
|
||||
// t_disguise = t_type is an identity for every monster a zap ray can
|
||||
// actually stop on: the one disguised kind, the xeroc, looks like an
|
||||
// item, and step_ok is true for item characters, so the ray walks
|
||||
// straight past it. Pinned anyway, because C assigns it.
|
||||
if tp.Disguise != tp.Type {
|
||||
t.Errorf("disguise = %q, want %q", tp.Disguise, tp.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapTeleportToPullsMonsterIn covers WS_TELTO: the victim lands on
|
||||
// hero + delta, which is the square next to the hero along the ray.
|
||||
func TestZapTeleportToPullsMonsterIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 22)
|
||||
hero := Coord{X: 5, Y: corridorY}
|
||||
placeHero(g, hero)
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
|
||||
from := Coord{X: 8, Y: corridorY}
|
||||
tp := putMonster(g, 'Z', from)
|
||||
|
||||
g.zapTeleport(zapWand(WandTeleportTo))
|
||||
|
||||
want := Coord{X: hero.X + 1, Y: hero.Y}
|
||||
if tp.Pos != want {
|
||||
t.Errorf("monster at %v after teleport-to, want %v", tp.Pos, want)
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(want.Y, want.X) != tp {
|
||||
t.Error("the map does not have the monster at its new spot")
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(from.Y, from.X) != nil {
|
||||
t.Error("the monster is still on the map where it came from")
|
||||
}
|
||||
|
||||
if tp.Dest != &g.Player.Pos {
|
||||
t.Error("the teleported monster is not chasing the hero")
|
||||
}
|
||||
|
||||
if !tp.On(Awake) {
|
||||
t.Error("the teleported monster was not woken")
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapTeleportAwayMovesMonsterOff covers WS_TELAWAY, whose C loop
|
||||
// re-draws until the spot is not the hero's own.
|
||||
func TestZapTeleportAwayMovesMonsterOff(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 23)
|
||||
hero := Coord{X: 5, Y: corridorY}
|
||||
placeHero(g, hero)
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
|
||||
from := Coord{X: 8, Y: corridorY}
|
||||
tp := putMonster(g, 'Z', from)
|
||||
|
||||
g.zapTeleport(zapWand(WandTeleportAway))
|
||||
|
||||
if tp.Pos == from {
|
||||
t.Error("teleport away did not move the monster")
|
||||
}
|
||||
|
||||
if tp.Pos == hero {
|
||||
t.Error("teleport away dropped the monster onto the hero")
|
||||
}
|
||||
|
||||
if g.Level.Char(tp.Pos.Y, tp.Pos.X) != Floor {
|
||||
t.Errorf("monster landed on %q, want floor",
|
||||
g.Level.Char(tp.Pos.Y, tp.Pos.X))
|
||||
}
|
||||
|
||||
if g.Level.MonsterAt(from.Y, from.X) != nil {
|
||||
t.Error("the monster is still on the map where it came from")
|
||||
}
|
||||
}
|
||||
|
||||
// vanishMsg is what C says when the missile finds nobody to hit, with
|
||||
// the original spelling of "missile" intact (sticks.c 191).
|
||||
//
|
||||
//nolint:misspell // C's spelling, kept faithfully
|
||||
const vanishMsg = "the missle vanishes with a puff of smoke"
|
||||
|
||||
// TestZapMagicMissile covers WS_MISSILE both ways: a victim that saves
|
||||
// gets C's puff-of-smoke message and no damage, one that does not is
|
||||
// hit by a bolt whose o_hplus of 100 cannot miss.
|
||||
func TestZapMagicMissile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lvl int
|
||||
wantMsg bool
|
||||
}{
|
||||
{name: "victim saves", lvl: saveProofLvl, wantMsg: true},
|
||||
{name: "victim is hit", lvl: 1, wantMsg: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 24)
|
||||
placeHero(g, Coord{X: 5, Y: corridorY})
|
||||
g.Delta = Coord{X: 1, Y: 0}
|
||||
|
||||
tp := putMonster(g, 'Z', Coord{X: 8, Y: corridorY})
|
||||
tp.Stats.Lvl = tt.lvl
|
||||
tp.Stats.HP = 500
|
||||
|
||||
pinRng(t, g, d20, 1) // the lowest save throw there is
|
||||
|
||||
g.zapMagicMissile(zapWand(WandMagicMissile))
|
||||
|
||||
if got := g.Msgs.Huh == vanishMsg; got != tt.wantMsg {
|
||||
t.Errorf("message = %q, want vanish = %v", g.Msgs.Huh, tt.wantMsg)
|
||||
}
|
||||
|
||||
if hurt := tp.Stats.HP < 500; hurt == tt.wantMsg {
|
||||
t.Errorf("hit points = %d, want damage = %v",
|
||||
tp.Stats.HP, !tt.wantMsg)
|
||||
}
|
||||
|
||||
if !g.Items.Sticks[WandMagicMissile].Know {
|
||||
t.Error("the magic missile wand did not identify itself")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFixStickDamage covers the strcmp against ws_type: a staff swings
|
||||
// for 2x3, everything else for 1x1, and both hurl for 1x1.
|
||||
func TestFixStickDamage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
material string
|
||||
want string
|
||||
}{
|
||||
{material: staffName, want: "2x3"},
|
||||
{material: wandName, want: "1x1"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.material, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 25)
|
||||
g.Items.WandType[WandCold] = tt.material
|
||||
|
||||
cur := newObject()
|
||||
cur.Kind = KindWand
|
||||
cur.Which = int(WandCold)
|
||||
g.fixStick(cur)
|
||||
|
||||
if !slices.Equal(cur.Damage, dice(tt.want)) {
|
||||
t.Errorf("damage = %v, want %v", cur.Damage, tt.want)
|
||||
}
|
||||
|
||||
if !slices.Equal(cur.HurlDmg, dice("1x1")) {
|
||||
t.Errorf("hurl damage = %v, want 1x1", cur.HurlDmg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFixStickCharges covers the charge switch. C is rnd(10)+10 for the
|
||||
// wand of light and rnd(5)+3 for everything else, so both ends of both
|
||||
// ranges must show up over enough draws and nothing outside them ever.
|
||||
func TestFixStickCharges(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
kind WandKind
|
||||
lo, hi int
|
||||
}{
|
||||
{name: "light", kind: WandLight, lo: 10, hi: 19},
|
||||
{name: "other", kind: WandCold, lo: 3, hi: 7},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 26)
|
||||
lo, hi := 1<<30, -1
|
||||
|
||||
for range 500 {
|
||||
cur := newObject()
|
||||
cur.Kind = KindWand
|
||||
cur.Which = int(tt.kind)
|
||||
g.fixStick(cur)
|
||||
lo = min(lo, cur.Charges)
|
||||
hi = max(hi, cur.Charges)
|
||||
}
|
||||
|
||||
if lo != tt.lo || hi != tt.hi {
|
||||
t.Errorf("charges ranged over %d..%d, want %d..%d",
|
||||
lo, hi, tt.lo, tt.hi)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestChargeStr covers sticks.c charge_str: nothing at all until the
|
||||
// stick is known, then the terse or verbose bracket.
|
||||
func TestChargeStr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
known bool
|
||||
terse bool
|
||||
want string
|
||||
}{
|
||||
{name: "unknown", known: false, terse: false, want: ""},
|
||||
{name: "unknown and terse", known: false, terse: true, want: ""},
|
||||
{name: "known", known: true, terse: false, want: " [7 charges]"},
|
||||
{name: "known and terse", known: true, terse: true, want: " [7]"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 27)
|
||||
g.Options.Terse = tt.terse
|
||||
|
||||
obj := zapWand(WandCold)
|
||||
obj.Charges = 7
|
||||
|
||||
if tt.known {
|
||||
obj.Flags.Set(Known)
|
||||
}
|
||||
|
||||
if got := chargeStr(g, obj); got != tt.want {
|
||||
t.Errorf("chargeStr = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestZapBoltNames covers the name each of the three bolt wands fires
|
||||
// under (sticks.c 225-231), read back out of the weapon table entry
|
||||
// fire_bolt overwrites and out of the bounce message.
|
||||
func TestZapBoltNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
kind WandKind
|
||||
want string
|
||||
}{
|
||||
{kind: WandLightning, want: boltName},
|
||||
{kind: WandFire, want: flameName},
|
||||
{kind: WandCold, want: iceName},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.want, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkCarvedGame(t, 28)
|
||||
placeHero(g, Coord{X: roomAX + 1, Y: corridorY})
|
||||
g.Player.Stats.Lvl = saveProofLvl // never hurt by the rebound
|
||||
g.Delta = Coord{X: -1, Y: 0} // straight at the west wall
|
||||
|
||||
g.zapBolt(zapWand(tt.kind))
|
||||
|
||||
if got := g.Items.Weapons[WeaponFlame].Name; got != tt.want {
|
||||
t.Errorf("weapon name = %q, want %q", got, tt.want)
|
||||
}
|
||||
|
||||
if !strings.Contains(g.Msgs.Huh, tt.want) {
|
||||
t.Errorf("message = %q, want it to name the %q",
|
||||
g.Msgs.Huh, tt.want)
|
||||
}
|
||||
|
||||
if !g.Items.Sticks[tt.kind].Know {
|
||||
t.Error("the bolt wand did not identify itself")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
934
game/tables.go
934
game/tables.go
@@ -1,934 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// tables.go — the static data tables the C game kept as file-scope globals
|
||||
// (extern.c, init.c, and assorted per-file statics). The port gathers them
|
||||
// into gameData: every RogueGame carries its own copy, so the package holds
|
||||
// no package-level state. Per-game mutable copies (the ObjInfo tables,
|
||||
// whose probabilities are re-summed and whose Know/Guess fields change
|
||||
// during play) are cloned into RogueGame.Items by NewGame.
|
||||
|
||||
// helpEntry is rogue.h struct h_list.
|
||||
type helpEntry struct {
|
||||
Ch byte
|
||||
Desc string
|
||||
Print bool
|
||||
}
|
||||
|
||||
// gameData is the bundle of static tables, built by newGameData and hung on
|
||||
// RogueGame as g.data. Nothing in it mutates during play: the one table the
|
||||
// C code writes to (the venus flytrap damage hack) operates on the game's
|
||||
// Monsters copy, not on this template.
|
||||
type gameData struct {
|
||||
// initStats is the C INIT_STATS: the player's starting statistics.
|
||||
initStats Stats
|
||||
|
||||
// aClass is extern.c a_class[]: armor class for each armor type.
|
||||
aClass [NumArmorTypes]int
|
||||
|
||||
// eLevels is extern.c e_levels[]: experience thresholds per level; the
|
||||
// zero terminates the table as in C.
|
||||
eLevels []int
|
||||
|
||||
// trName is extern.c tr_name[]: names of the traps.
|
||||
trName [NumTrapTypes]string
|
||||
|
||||
// invTName is extern.c inv_t_name[]: the inventory style names.
|
||||
invTName []string
|
||||
|
||||
// monsterTable is extern.c monsters[26]: all monster kinds, indexed by
|
||||
// letter - 'A'. Monster strength (XX in C) is always 10; HP (___ in C)
|
||||
// is rolled from the level at creation time.
|
||||
monsterTable [26]MonsterKind
|
||||
|
||||
// Base ObjInfo tables (extern.c). These are templates: NewGame copies
|
||||
// them into ItemLore before initProbs converts Prob to cumulative form.
|
||||
baseThings [NumThings]ObjInfo
|
||||
baseArmInfo [NumArmorTypes]ObjInfo
|
||||
basePotInfo [NumPotionTypes]ObjInfo
|
||||
baseRingInfo [NumRingTypes]ObjInfo
|
||||
baseScrInfo [NumScrollTypes]ObjInfo
|
||||
baseWeapInfo [NumWeaponTypes + 1]ObjInfo
|
||||
baseWsInfo [NumWandTypes]ObjInfo
|
||||
|
||||
// rainbow is init.c rainbow[]: the possible potion colors.
|
||||
rainbow []string
|
||||
|
||||
// sylls is init.c sylls[]: syllables for generated scroll names.
|
||||
sylls []string
|
||||
|
||||
// stoneTable is init.c stones[]: ring stones and their worth.
|
||||
stoneTable []Stone
|
||||
|
||||
// woods is init.c wood[]: what staffs are made of.
|
||||
woods []string
|
||||
|
||||
// metals is init.c metal[]: what wands are made of.
|
||||
metals []string
|
||||
|
||||
// helpStr is extern.c helpstr[]: the '?' command help text.
|
||||
helpStr []helpEntry
|
||||
|
||||
// hNames are the strings for hitting; the first four are used when the
|
||||
// player strikes, the second four for monsters (fight.c h_names).
|
||||
hNames [8]string
|
||||
|
||||
// mNames are the strings for missing (fight.c m_names).
|
||||
mNames [8]string
|
||||
|
||||
// strPlus adjusts hit probabilities due to strength (fight.c str_plus).
|
||||
strPlus [32]int
|
||||
|
||||
// addDam adjusts damage done due to strength (fight.c add_dam).
|
||||
addDam [32]int
|
||||
|
||||
// lvlMons and wandMons list monsters in rough order of vorpalness;
|
||||
// zero entries in wandMons never wander (monsters.c).
|
||||
lvlMons [26]byte
|
||||
wandMons [26]byte
|
||||
|
||||
// ringUses is the rings.c ring_eat static uses[] table: how much food
|
||||
// each ring type uses up per turn (negative = a 1-in-n chance of 1).
|
||||
ringUses [NumRingTypes]int
|
||||
|
||||
// initWeaps is the weapons.c init_dam[] table.
|
||||
initWeaps [NumWeaponTypes]weaponSetup
|
||||
|
||||
// pActions is potions.c p_actions[]. The P_SEEINVIS message is dynamic
|
||||
// (it names the fruit) and is computed in applyPotionFuse.
|
||||
pActions [NumPotionTypes]pact
|
||||
|
||||
// idType maps identify scrolls to the kind of item they identify
|
||||
// (scrolls.c static id_type).
|
||||
idType [ScrollIdentifyRingOrStick + 1]ObjectKind
|
||||
|
||||
// rdesConn is the hardcoded 3x3 room adjacency matrix from
|
||||
// passages.c do_passages.
|
||||
rdesConn [MaxRooms][MaxRooms]bool
|
||||
|
||||
// thingList is misc.c rnd_thing()'s static table.
|
||||
thingList []byte
|
||||
|
||||
// identList is command.c's static ident_list.
|
||||
identList []helpEntry
|
||||
|
||||
// hungerStateName is io.c state_name[].
|
||||
hungerStateName [4]string
|
||||
|
||||
// ripArt is the rip.c rip[] tombstone art.
|
||||
ripArt []string
|
||||
|
||||
// killnameTable is the rip.c nlist[]: special death causes.
|
||||
killnameTable []helpEntry
|
||||
|
||||
// scoreReasons is the rip.c reason[] scoreboard strings.
|
||||
scoreReasons [4]string
|
||||
|
||||
// quaffHandlers dispatches each potion kind to its effect method:
|
||||
// the cases of the potions.c quaff switch. The bool is trip — was
|
||||
// the hero hallucinating when the potion went down.
|
||||
quaffHandlers [NumPotionTypes]func(g *RogueGame, trip bool)
|
||||
|
||||
// readHandlers dispatches each scroll kind to its effect method:
|
||||
// the cases of the scrolls.c read_scroll switch. The handler gets
|
||||
// the scroll being read (the identify family needs its subtype).
|
||||
readHandlers [NumScrollTypes]func(g *RogueGame, obj *Object)
|
||||
|
||||
// zapHandlers dispatches each wand kind to its effect method: the
|
||||
// cases of the sticks.c do_zap switch. A false return aborts the
|
||||
// zap without using a charge (drain life on a too-weak hero).
|
||||
zapHandlers [NumWandTypes]func(g *RogueGame, obj *Object) bool
|
||||
|
||||
// hitHandlers dispatches a monster's special power when its hit
|
||||
// lands, indexed by monster letter - 'A': the cases of the fight.c
|
||||
// attack switch. A true return means the monster removed itself.
|
||||
hitHandlers [26]func(g *RogueGame, mp *Monster, mname string) bool
|
||||
|
||||
// commandHandlers dispatches the ordinary command keys: the simple
|
||||
// cases of the big command.c switch. Keys that re-dispatch (runs,
|
||||
// fight, repeat, move-on) and wizard keys stay in dispatchKey.
|
||||
commandHandlers map[byte]func(g *RogueGame)
|
||||
|
||||
// trapHandlers dispatches each sprung trap to its effect method:
|
||||
// the cases of the move.c be_trapped switch.
|
||||
trapHandlers [NumTrapTypes]func(g *RogueGame, tc Coord)
|
||||
|
||||
// daemonHandlers dispatches DaemonIDs to their callbacks: the C
|
||||
// d_func function pointers (daemons.c / daemon.c).
|
||||
daemonHandlers [DTurnSee + 1]func(g *RogueGame, arg int)
|
||||
}
|
||||
|
||||
// ripWall is the repeated blank wall line of the tombstone art.
|
||||
const ripWall = " | |"
|
||||
|
||||
// newGameData builds the static tables. Each game gets a fresh copy, which
|
||||
// keeps the package free of globals.
|
||||
//
|
||||
//nolint:funlen,maintidx // a single composite literal holding every C data table
|
||||
func newGameData() *gameData {
|
||||
return &gameData{
|
||||
initStats: Stats{
|
||||
Str: 16, Exp: 0, Lvl: 1, ArmorClass: 10, HP: 12,
|
||||
Dmg: dice("1x4"), MaxHP: 12,
|
||||
},
|
||||
|
||||
aClass: [NumArmorTypes]int{
|
||||
8, // LEATHER
|
||||
7, // RING_MAIL
|
||||
7, // STUDDED_LEATHER
|
||||
6, // SCALE_MAIL
|
||||
5, // CHAIN_MAIL
|
||||
4, // SPLINT_MAIL
|
||||
4, // BANDED_MAIL
|
||||
3, // PLATE_MAIL
|
||||
},
|
||||
|
||||
eLevels: []int{
|
||||
10, 20, 40, 80, 160, 320, 640, 1300, 2600, 5200, 13000, 26000,
|
||||
50000, 100000, 200000, 400000, 800000, 2000000, 4000000, 8000000, 0,
|
||||
},
|
||||
|
||||
trName: [NumTrapTypes]string{
|
||||
"a trapdoor",
|
||||
"an arrow trap",
|
||||
"a sleeping gas trap",
|
||||
"a beartrap",
|
||||
"a teleport trap",
|
||||
"a poison dart trap",
|
||||
"a rust trap",
|
||||
"a mysterious trap",
|
||||
},
|
||||
|
||||
invTName: []string{"Overwrite", "Slow", "Clear"},
|
||||
|
||||
monsterTable: [26]MonsterKind{
|
||||
/* Name CARRY FLAGS str exp lvl arm hp dmg */
|
||||
{"aquator", 0, Mean, Stats{10, 20, 5, 2, 1, dice("0x0/0x0"), 0}},
|
||||
{"bat", 0, Flying, Stats{10, 1, 1, 3, 1, dice("1x2"), 0}},
|
||||
{"centaur", 15, 0, Stats{10, 17, 4, 4, 1, dice("1x2/1x5/1x5"), 0}},
|
||||
{"dragon", 100, Mean, Stats{10, 5000, 10, -1, 1, dice("1x8/1x8/3x10"), 0}},
|
||||
{"emu", 0, Mean, Stats{10, 2, 1, 7, 1, dice("1x2"), 0}},
|
||||
{"venus flytrap", 0, Mean, Stats{10, 80, 8, 3, 1, dice("%%%x0"), 0}},
|
||||
{"griffin", 20, Mean | Flying | Regenerates,
|
||||
Stats{10, 2000, 13, 2, 1, dice("4x3/3x5"), 0}},
|
||||
{"hobgoblin", 0, Mean, Stats{10, 3, 1, 5, 1, dice("1x8"), 0}},
|
||||
{"ice monster", 0, 0, Stats{10, 5, 1, 9, 1, dice("0x0"), 0}},
|
||||
{"jabberwock", 70, 0, Stats{10, 3000, 15, 6, 1, dice("2x12/2x4"), 0}},
|
||||
{"kestrel", 0, Mean | Flying, Stats{10, 1, 1, 7, 1, dice("1x4"), 0}},
|
||||
{"leprechaun", 0, 0, Stats{10, 10, 3, 8, 1, dice("1x1"), 0}},
|
||||
{"medusa", 40, Mean, Stats{10, 200, 8, 2, 1, dice("3x4/3x4/2x5"), 0}},
|
||||
{"nymph", 100, 0, Stats{10, 37, 3, 9, 1, dice("0x0"), 0}},
|
||||
{"orc", 15, Greedy, Stats{10, 5, 1, 6, 1, dice("1x8"), 0}},
|
||||
{"phantom", 0, Invisible, Stats{10, 120, 8, 3, 1, dice("4x4"), 0}},
|
||||
{"quagga", 0, Mean, Stats{10, 15, 3, 3, 1, dice("1x5/1x5"), 0}},
|
||||
{"rattlesnake", 0, Mean, Stats{10, 9, 2, 3, 1, dice("1x6"), 0}},
|
||||
{"snake", 0, Mean, Stats{10, 2, 1, 5, 1, dice("1x3"), 0}},
|
||||
{"troll", 50, Regenerates | Mean, Stats{10, 120, 6, 4, 1, dice("1x8/1x8/2x6"), 0}},
|
||||
{"black unicorn", 0, Mean, Stats{10, 190, 7, -2, 1, dice("1x9/1x9/2x9"), 0}},
|
||||
{"vampire", 20, Regenerates | Mean, Stats{10, 350, 8, 1, 1, dice("1x10"), 0}},
|
||||
{"wraith", 0, 0, Stats{10, 55, 5, 4, 1, dice("1x6"), 0}},
|
||||
{"xeroc", 30, 0, Stats{10, 100, 7, 7, 1, dice("4x4"), 0}},
|
||||
{"yeti", 30, 0, Stats{10, 50, 4, 6, 1, dice("1x6/1x6"), 0}},
|
||||
{"zombie", 0, Mean, Stats{10, 6, 2, 8, 1, dice("1x8"), 0}},
|
||||
},
|
||||
|
||||
baseThings: [NumThings]ObjInfo{
|
||||
{Prob: 26}, // potion
|
||||
{Prob: 36}, // scroll
|
||||
{Prob: 16}, // food
|
||||
{Prob: 7}, // weapon
|
||||
{Prob: 7}, // armor
|
||||
{Prob: 4}, // ring
|
||||
{Prob: 4}, // stick
|
||||
},
|
||||
|
||||
baseArmInfo: [NumArmorTypes]ObjInfo{
|
||||
{Name: "leather armor", Prob: 20, Worth: 20},
|
||||
{Name: "ring mail", Prob: 15, Worth: 25},
|
||||
{Name: "studded leather armor", Prob: 15, Worth: 20},
|
||||
{Name: "scale mail", Prob: 13, Worth: 30},
|
||||
{Name: "chain mail", Prob: 12, Worth: 75},
|
||||
{Name: "splint mail", Prob: 10, Worth: 80},
|
||||
{Name: "banded mail", Prob: 10, Worth: 90},
|
||||
{Name: "plate mail", Prob: 5, Worth: 150},
|
||||
},
|
||||
|
||||
//nolint:dupl // distinct C data tables that merely share their shape
|
||||
basePotInfo: [NumPotionTypes]ObjInfo{
|
||||
{Name: "confusion", Prob: 7, Worth: 5},
|
||||
{Name: "hallucination", Prob: 8, Worth: 5},
|
||||
{Name: "poison", Prob: 8, Worth: 5},
|
||||
{Name: "gain strength", Prob: 13, Worth: 150},
|
||||
{Name: "see invisible", Prob: 3, Worth: 100},
|
||||
{Name: "healing", Prob: 13, Worth: 130},
|
||||
{Name: "monster detection", Prob: 6, Worth: 130},
|
||||
{Name: "magic detection", Prob: 6, Worth: 105},
|
||||
{Name: "raise level", Prob: 2, Worth: 250},
|
||||
{Name: "extra healing", Prob: 5, Worth: 200},
|
||||
{Name: "haste self", Prob: 5, Worth: 190},
|
||||
{Name: "restore strength", Prob: 13, Worth: 130},
|
||||
{Name: "blindness", Prob: 5, Worth: 5},
|
||||
{Name: "levitation", Prob: 6, Worth: 75},
|
||||
},
|
||||
|
||||
//nolint:dupl // distinct C data tables that merely share their shape
|
||||
baseRingInfo: [NumRingTypes]ObjInfo{
|
||||
{Name: "protection", Prob: 9, Worth: 400},
|
||||
{Name: "add strength", Prob: 9, Worth: 400},
|
||||
{Name: "sustain strength", Prob: 5, Worth: 280},
|
||||
{Name: "searching", Prob: 10, Worth: 420},
|
||||
{Name: "see invisible", Prob: 10, Worth: 310},
|
||||
{Name: "adornment", Prob: 1, Worth: 10},
|
||||
{Name: "aggravate monster", Prob: 10, Worth: 10},
|
||||
{Name: "dexterity", Prob: 8, Worth: 440},
|
||||
{Name: "increase damage", Prob: 8, Worth: 400},
|
||||
{Name: "regeneration", Prob: 4, Worth: 460},
|
||||
{Name: "slow digestion", Prob: 9, Worth: 240},
|
||||
{Name: "teleportation", Prob: 5, Worth: 30},
|
||||
{Name: "stealth", Prob: 7, Worth: 470},
|
||||
{Name: "maintain armor", Prob: 5, Worth: 380},
|
||||
},
|
||||
|
||||
baseScrInfo: [NumScrollTypes]ObjInfo{
|
||||
{Name: "monster confusion", Prob: 7, Worth: 140},
|
||||
{Name: "magic mapping", Prob: 4, Worth: 150},
|
||||
{Name: "hold monster", Prob: 2, Worth: 180},
|
||||
{Name: "sleep", Prob: 3, Worth: 5},
|
||||
{Name: "enchant armor", Prob: 7, Worth: 160},
|
||||
{Name: "identify potion", Prob: 10, Worth: 80},
|
||||
{Name: "identify scroll", Prob: 10, Worth: 80},
|
||||
{Name: "identify weapon", Prob: 6, Worth: 80},
|
||||
{Name: "identify armor", Prob: 7, Worth: 100},
|
||||
{Name: "identify ring, wand or staff", Prob: 10, Worth: 115},
|
||||
{Name: "scare monster", Prob: 3, Worth: 200},
|
||||
{Name: "food detection", Prob: 2, Worth: 60},
|
||||
{Name: "teleportation", Prob: 5, Worth: 165},
|
||||
{Name: "enchant weapon", Prob: 8, Worth: 150},
|
||||
{Name: "create monster", Prob: 4, Worth: 75},
|
||||
{Name: "remove curse", Prob: 7, Worth: 105},
|
||||
{Name: "aggravate monsters", Prob: 3, Worth: 20},
|
||||
{Name: "protect armor", Prob: 2, Worth: 250},
|
||||
},
|
||||
|
||||
baseWeapInfo: [NumWeaponTypes + 1]ObjInfo{
|
||||
{Name: "mace", Prob: 11, Worth: 8},
|
||||
{Name: "long sword", Prob: 11, Worth: 15},
|
||||
{Name: "short bow", Prob: 12, Worth: 15},
|
||||
{Name: "arrow", Prob: 12, Worth: 1},
|
||||
{Name: "dagger", Prob: 8, Worth: 3},
|
||||
{Name: "two handed sword", Prob: 10, Worth: 75},
|
||||
{Name: "dart", Prob: 12, Worth: 2},
|
||||
{Name: "shuriken", Prob: 12, Worth: 5},
|
||||
{Name: "spear", Prob: 12, Worth: 5},
|
||||
{}, // DO NOT REMOVE: fake entry for dragon's breath
|
||||
},
|
||||
|
||||
//nolint:dupl // distinct C data tables that merely share their shape
|
||||
baseWsInfo: [NumWandTypes]ObjInfo{
|
||||
{Name: "light", Prob: 12, Worth: 250},
|
||||
{Name: "invisibility", Prob: 6, Worth: 5},
|
||||
{Name: "lightning", Prob: 3, Worth: 330},
|
||||
{Name: "fire", Prob: 3, Worth: 330},
|
||||
{Name: "cold", Prob: 3, Worth: 330},
|
||||
{Name: "polymorph", Prob: 15, Worth: 310},
|
||||
{Name: "magic missile", Prob: 10, Worth: 170},
|
||||
{Name: "haste monster", Prob: 10, Worth: 5},
|
||||
{Name: "slow monster", Prob: 11, Worth: 350},
|
||||
{Name: "drain life", Prob: 9, Worth: 300},
|
||||
{Name: "nothing", Prob: 1, Worth: 5},
|
||||
{Name: "teleport away", Prob: 6, Worth: 340},
|
||||
{Name: "teleport to", Prob: 6, Worth: 50},
|
||||
{Name: "cancellation", Prob: 5, Worth: 280},
|
||||
},
|
||||
|
||||
rainbow: []string{
|
||||
"amber", "aquamarine", "black", "blue", "brown", "clear", "crimson",
|
||||
"cyan", "ecru", "gold", "green", "grey", "magenta", "orange", "pink",
|
||||
"plaid", "purple", "red", "silver", "tan", "tangerine", "topaz",
|
||||
"turquoise", "vermilion", "violet", "white", "yellow",
|
||||
},
|
||||
|
||||
//nolint:misspell // "ther" is a C scroll syllable, kept faithfully
|
||||
sylls: []string{
|
||||
"a", "ab", "ag", "aks", "ala", "an", "app", "arg", "arze", "ash",
|
||||
"bek", "bie", "bit", "bjor", "blu", "bot", "bu", "byt", "comp",
|
||||
"con", "cos", "cre", "dalf", "dan", "den", "do", "e", "eep", "el",
|
||||
"eng", "er", "ere", "erk", "esh", "evs", "fa", "fid", "fri", "fu",
|
||||
"gan", "gar", "glen", "gop", "gre", "ha", "hyd", "i", "ing", "ip",
|
||||
"ish", "it", "ite", "iv", "jo", "kho", "kli", "klis", "la", "lech",
|
||||
"mar", "me", "mi", "mic", "mik", "mon", "mung", "mur", "nej",
|
||||
"nelg", "nep", "ner", "nes", "nes", "nih", "nin", "o", "od", "ood",
|
||||
"org", "orn", "ox", "oxy", "pay", "ple", "plu", "po", "pot",
|
||||
"prok", "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa",
|
||||
"san", "sat", "sef", "seh", "shu", "ski", "sna", "sne", "snik",
|
||||
"sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", "tem",
|
||||
"ther", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un",
|
||||
"uni", "ur", "val", "viv", "vly", "vom", "wah", "wed", "werg",
|
||||
"wex", "whon", "wun", "xo", "y", "yot", "yu", "zant", "zeb", "zim",
|
||||
"zok", "zon", "zum",
|
||||
},
|
||||
|
||||
stoneTable: []Stone{
|
||||
{"agate", 25}, {"alexandrite", 40}, {"amethyst", 50},
|
||||
{"carnelian", 40}, {"diamond", 300}, {"emerald", 300},
|
||||
{"germanium", 225}, {"granite", 5}, {"garnet", 50},
|
||||
{"jade", 150}, {"kryptonite", 300}, {"lapis lazuli", 50},
|
||||
{"moonstone", 50}, {"obsidian", 15}, {"onyx", 60},
|
||||
{"opal", 200}, {"pearl", 220}, {"peridot", 63},
|
||||
{"ruby", 350}, {"sapphire", 285}, {"stibotantalite", 200},
|
||||
{"tiger eye", 50}, {"topaz", 60}, {"turquoise", 70},
|
||||
{"taaffeite", 300}, {"zircon", 80},
|
||||
},
|
||||
|
||||
woods: []string{
|
||||
"avocado wood", "balsa", "bamboo", "banyan", "birch", "cedar",
|
||||
"cherry", "cinnibar", "cypress", "dogwood", "driftwood", "ebony",
|
||||
"elm", "eucalyptus", "fall", "hemlock", "holly", "ironwood",
|
||||
"kukui wood", "mahogany", "manzanita", "maple", "oaken",
|
||||
"persimmon wood", "pecan", "pine", "poplar", "redwood", "rosewood",
|
||||
"spruce", "teak", "walnut", "zebrawood",
|
||||
},
|
||||
|
||||
metals: []string{
|
||||
"aluminum", "beryllium", "bone", "brass", "bronze", "copper",
|
||||
"electrum", "gold", "iron", "lead", "magnesium", "mercury",
|
||||
"nickel", "pewter", "platinum", "steel", "silver", "silicon",
|
||||
"tin", "titanium", "tungsten", "zinc",
|
||||
},
|
||||
|
||||
helpStr: []helpEntry{
|
||||
{'?', " prints help", true},
|
||||
{'/', " identify object", true},
|
||||
{'h', " left", true},
|
||||
{'j', " down", true},
|
||||
{'k', " up", true},
|
||||
{'l', " right", true},
|
||||
{'y', " up & left", true},
|
||||
{'u', " up & right", true},
|
||||
{'b', " down & left", true},
|
||||
{'n', " down & right", true},
|
||||
{'H', " run left", false},
|
||||
{'J', " run down", false},
|
||||
{'K', " run up", false},
|
||||
{'L', " run right", false},
|
||||
{'Y', " run up & left", false},
|
||||
{'U', " run up & right", false},
|
||||
{'B', " run down & left", false},
|
||||
{'N', " run down & right", false},
|
||||
{CTRL('H'), " run left until adjacent", false},
|
||||
{CTRL('J'), " run down until adjacent", false},
|
||||
{CTRL('K'), " run up until adjacent", false},
|
||||
{CTRL('L'), " run right until adjacent", false},
|
||||
{CTRL('Y'), " run up & left until adjacent", false},
|
||||
{CTRL('U'), " run up & right until adjacent", false},
|
||||
{CTRL('B'), " run down & left until adjacent", false},
|
||||
{CTRL('N'), " run down & right until adjacent", false},
|
||||
{0, " <SHIFT><dir>: run that way", true},
|
||||
{0, " <CTRL><dir>: run till adjacent", true},
|
||||
{'f', "<dir> fight till death or near death", true},
|
||||
{'t', "<dir> throw something", true},
|
||||
{'m', "<dir> move onto without picking up", true},
|
||||
{'z', "<dir> zap a wand in a direction", true},
|
||||
{'^', "<dir> identify trap type", true},
|
||||
{'s', " search for trap/secret door", true},
|
||||
{'>', " go down a staircase", true},
|
||||
{'<', " go up a staircase", true},
|
||||
{'.', " rest for a turn", true},
|
||||
{',', " pick something up", true},
|
||||
{'i', " inventory", true},
|
||||
{'I', " inventory single item", true},
|
||||
{'q', " quaff potion", true},
|
||||
{'r', " read scroll", true},
|
||||
{'e', " eat food", true},
|
||||
{'w', " wield a weapon", true},
|
||||
{'W', " wear armor", true},
|
||||
{'T', " take armor off", true},
|
||||
{'P', " put on ring", true},
|
||||
{'R', " remove ring", true},
|
||||
{'d', " drop object", true},
|
||||
{'c', " call object", true},
|
||||
{'a', " repeat last command", true},
|
||||
{')', " print current weapon", true},
|
||||
{']', " print current armor", true},
|
||||
{'=', " print current rings", true},
|
||||
{'@', " print current stats", true},
|
||||
{'D', " recall what's been discovered", true},
|
||||
{'o', " examine/set options", true},
|
||||
{CTRL('R'), " redraw screen", true},
|
||||
{CTRL('P'), " repeat last message", true},
|
||||
{Escape, " cancel command", true},
|
||||
{'S', " save game", true},
|
||||
{'Q', " quit", true},
|
||||
{'!', " shell escape", true},
|
||||
{'F', "<dir> fight till either of you dies", true},
|
||||
{'v', " print version number", true},
|
||||
},
|
||||
|
||||
hNames: [8]string{
|
||||
" scored an excellent hit on ",
|
||||
" hit ",
|
||||
" have injured ",
|
||||
" swing and hit ",
|
||||
" scored an excellent hit on ",
|
||||
" hit ",
|
||||
" has injured ",
|
||||
" swings and hits ",
|
||||
},
|
||||
|
||||
mNames: [8]string{
|
||||
" miss",
|
||||
" swing and miss",
|
||||
" barely miss",
|
||||
" don't hit",
|
||||
" misses",
|
||||
" swings and misses",
|
||||
" barely misses",
|
||||
" doesn't hit",
|
||||
},
|
||||
|
||||
strPlus: [32]int{
|
||||
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
|
||||
},
|
||||
|
||||
addDam: [32]int{
|
||||
-7, -6, -5, -4, -3, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3,
|
||||
3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6,
|
||||
},
|
||||
|
||||
lvlMons: [26]byte{
|
||||
'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A',
|
||||
'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D',
|
||||
},
|
||||
|
||||
wandMons: [26]byte{
|
||||
'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A',
|
||||
0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0,
|
||||
},
|
||||
|
||||
ringUses: [NumRingTypes]int{
|
||||
1, // R_PROTECT
|
||||
1, // R_ADDSTR
|
||||
1, // R_SUSTSTR
|
||||
-3, // R_SEARCH
|
||||
-5, // R_SEEINVIS
|
||||
0, // R_NOP
|
||||
0, // R_AGGR
|
||||
-3, // R_ADDHIT
|
||||
-3, // R_ADDDAM
|
||||
2, // R_REGEN
|
||||
-2, // R_DIGEST
|
||||
0, // R_TELEPORT
|
||||
1, // R_STEALTH
|
||||
1, // R_SUSTARM
|
||||
},
|
||||
|
||||
initWeaps: [NumWeaponTypes]weaponSetup{
|
||||
{dice("2x4"), dice("1x3"), noWeapon, 0}, // WeaponMace
|
||||
{dice("3x4"), dice("1x2"), noWeapon, 0}, // Long sword
|
||||
{dice("1x1"), dice("1x1"), noWeapon, 0}, // WeaponBow
|
||||
{dice("1x1"), dice("2x3"), WeaponBow, Stackable | Missile}, // WeaponArrow
|
||||
{dice("1x6"), dice("1x4"), noWeapon, Missile}, // WeaponDagger
|
||||
{dice("4x4"), dice("1x2"), noWeapon, 0}, // 2h sword
|
||||
{dice("1x1"), dice("1x3"), noWeapon, Stackable | Missile}, // WeaponDart
|
||||
{dice("1x2"), dice("2x4"), noWeapon, Stackable | Missile}, // Shuriken
|
||||
{dice("2x3"), dice("1x6"), noWeapon, Missile}, // WeaponSpear
|
||||
},
|
||||
|
||||
pActions: [NumPotionTypes]pact{
|
||||
PotionConfusion: {Confused, DUnconfuse, HuhDuration,
|
||||
"what a tripy feeling!",
|
||||
"wait, what's going on here. Huh? What? Who?"},
|
||||
PotionLSD: {Hallucinating, DComeDown, SeeDuration,
|
||||
"Oh, wow! Everything seems so cosmic!",
|
||||
"Oh, wow! Everything seems so cosmic!"},
|
||||
PotionSeeInvisible: {CanSeeInvisible, DUnsee, SeeDuration, "", ""},
|
||||
PotionBlindness: {Blind, DSight, SeeDuration,
|
||||
"oh, bummer! Everything is dark! Help!",
|
||||
"a cloak of darkness falls around you"},
|
||||
PotionLevitation: {Levitating, DLand, HealTime,
|
||||
"oh, wow! You're floating in the air!",
|
||||
"you start to float in the air"},
|
||||
},
|
||||
|
||||
idType: [ScrollIdentifyRingOrStick + 1]ObjectKind{
|
||||
ScrollIdentifyPotion: KindPotion,
|
||||
ScrollIdentifyScroll: KindScroll,
|
||||
ScrollIdentifyWeapon: KindWeapon,
|
||||
ScrollIdentifyArmor: KindArmor,
|
||||
ScrollIdentifyRingOrStick: KindRingOrStick,
|
||||
},
|
||||
|
||||
rdesConn: [MaxRooms][MaxRooms]bool{
|
||||
{false, true, false, true, false, false, false, false, false},
|
||||
{true, false, true, false, true, false, false, false, false},
|
||||
{false, true, false, false, false, true, false, false, false},
|
||||
{true, false, false, false, true, false, true, false, false},
|
||||
{false, true, false, true, false, true, false, true, false},
|
||||
{false, false, true, false, true, false, false, false, true},
|
||||
{false, false, false, true, false, false, false, true, false},
|
||||
{false, false, false, false, true, false, true, false, true},
|
||||
{false, false, false, false, false, true, false, true, false},
|
||||
},
|
||||
|
||||
thingList: []byte{
|
||||
Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet,
|
||||
},
|
||||
|
||||
identList: []helpEntry{
|
||||
{'|', "wall of a room", false},
|
||||
{'-', "wall of a room", false},
|
||||
{Gold, goldName, false},
|
||||
{Stairs, "a staircase", false},
|
||||
{Door, "door", false},
|
||||
{Floor, "room floor", false},
|
||||
{PlayerCh, "you", false},
|
||||
{Passage, "passage", false},
|
||||
{Trap, "trap", false},
|
||||
{Potion, potionName, false},
|
||||
{Scroll, scrollName, false},
|
||||
{Food, "food", false},
|
||||
{Weapon, "weapon", false},
|
||||
{' ', "solid rock", false},
|
||||
{Armor, "armor", false},
|
||||
{Amulet, "the Amulet of Yendor", false},
|
||||
{Ring, ringName, false},
|
||||
{Stick, "wand or staff", false},
|
||||
},
|
||||
|
||||
hungerStateName: [4]string{"", "Hungry", "Weak", "Faint"},
|
||||
|
||||
ripArt: []string{
|
||||
" __________",
|
||||
" / \\",
|
||||
" / REST \\",
|
||||
" / IN \\",
|
||||
" / PEACE \\",
|
||||
" / \\",
|
||||
ripWall,
|
||||
ripWall,
|
||||
" | killed by a |",
|
||||
ripWall,
|
||||
" | 1980 |",
|
||||
" *| * * * | *",
|
||||
" ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______",
|
||||
},
|
||||
|
||||
killnameTable: []helpEntry{
|
||||
{'a', "arrow", true},
|
||||
{'b', "bolt", true},
|
||||
{'d', "dart", true},
|
||||
{'h', "hypothermia", false},
|
||||
{'s', "starvation", false},
|
||||
},
|
||||
|
||||
scoreReasons: [4]string{
|
||||
"killed",
|
||||
"quit",
|
||||
"A total winner",
|
||||
"killed with Amulet",
|
||||
},
|
||||
|
||||
quaffHandlers: [NumPotionTypes]func(*RogueGame, bool){
|
||||
PotionConfusion: (*RogueGame).quaffConfusion,
|
||||
PotionLSD: (*RogueGame).quaffLSD,
|
||||
PotionPoison: (*RogueGame).quaffPoison,
|
||||
PotionGainStrength: (*RogueGame).quaffGainStrength,
|
||||
PotionSeeInvisible: (*RogueGame).quaffSeeInvisible,
|
||||
PotionHealing: (*RogueGame).quaffHealing,
|
||||
PotionDetectMonsters: (*RogueGame).quaffDetectMonsters,
|
||||
PotionDetectMagic: (*RogueGame).quaffDetectMagic,
|
||||
PotionRaiseLevel: (*RogueGame).quaffRaiseLevel,
|
||||
PotionExtraHealing: (*RogueGame).quaffExtraHealing,
|
||||
PotionHaste: (*RogueGame).quaffHaste,
|
||||
PotionRestoreStrength: (*RogueGame).quaffRestoreStrength,
|
||||
PotionBlindness: (*RogueGame).quaffBlindness,
|
||||
PotionLevitation: (*RogueGame).quaffLevitation,
|
||||
},
|
||||
|
||||
readHandlers: [NumScrollTypes]func(*RogueGame, *Object){
|
||||
ScrollMonsterConfusion: (*RogueGame).readMonsterConfusion,
|
||||
ScrollMagicMapping: (*RogueGame).readMagicMapping,
|
||||
ScrollHoldMonster: (*RogueGame).readHoldMonster,
|
||||
ScrollSleep: (*RogueGame).readSleep,
|
||||
ScrollEnchantArmor: (*RogueGame).readEnchantArmor,
|
||||
ScrollIdentifyPotion: (*RogueGame).readIdentify,
|
||||
ScrollIdentifyScroll: (*RogueGame).readIdentify,
|
||||
ScrollIdentifyWeapon: (*RogueGame).readIdentify,
|
||||
ScrollIdentifyArmor: (*RogueGame).readIdentify,
|
||||
ScrollIdentifyRingOrStick: (*RogueGame).readIdentify,
|
||||
ScrollScareMonster: (*RogueGame).readScareMonster,
|
||||
ScrollFoodDetection: (*RogueGame).readFoodDetection,
|
||||
ScrollTeleportation: (*RogueGame).readTeleportation,
|
||||
ScrollEnchantWeapon: (*RogueGame).readEnchantWeapon,
|
||||
ScrollCreateMonster: (*RogueGame).readCreateMonster,
|
||||
ScrollRemoveCurse: (*RogueGame).readRemoveCurse,
|
||||
ScrollAggravateMonsters: (*RogueGame).readAggravateMonsters,
|
||||
ScrollProtectArmor: (*RogueGame).readProtectArmor,
|
||||
},
|
||||
|
||||
zapHandlers: [NumWandTypes]func(*RogueGame, *Object) bool{
|
||||
WandLight: (*RogueGame).zapLight,
|
||||
WandInvisibility: (*RogueGame).zapInvisibility,
|
||||
WandLightning: (*RogueGame).zapBolt,
|
||||
WandFire: (*RogueGame).zapBolt,
|
||||
WandCold: (*RogueGame).zapBolt,
|
||||
WandPolymorph: (*RogueGame).zapPolymorph,
|
||||
WandMagicMissile: (*RogueGame).zapMagicMissile,
|
||||
WandHasteMonster: (*RogueGame).zapSpeed,
|
||||
WandSlowMonster: (*RogueGame).zapSpeed,
|
||||
WandDrainLife: (*RogueGame).zapDrainLife,
|
||||
WandTeleportAway: (*RogueGame).zapTeleport,
|
||||
WandTeleportTo: (*RogueGame).zapTeleport,
|
||||
WandCancellation: (*RogueGame).zapCancellation,
|
||||
},
|
||||
|
||||
hitHandlers: [26]func(*RogueGame, *Monster, string) bool{
|
||||
0: (*RogueGame).hitAquator, // 'A' - 'A'
|
||||
'F' - 'A': (*RogueGame).hitFlytrap,
|
||||
'I' - 'A': (*RogueGame).hitIceMonster,
|
||||
'L' - 'A': (*RogueGame).hitLeprechaun,
|
||||
'N' - 'A': (*RogueGame).hitNymph,
|
||||
'R' - 'A': (*RogueGame).hitRattlesnake,
|
||||
'V' - 'A': (*RogueGame).hitLifeDrainer,
|
||||
'W' - 'A': (*RogueGame).hitLifeDrainer,
|
||||
},
|
||||
|
||||
commandHandlers: map[byte]func(*RogueGame){
|
||||
',': (*RogueGame).pickupCommand,
|
||||
'!': (*RogueGame).shell,
|
||||
'h': func(g *RogueGame) { g.moveHero(0, -1) },
|
||||
'j': func(g *RogueGame) { g.moveHero(1, 0) },
|
||||
'k': func(g *RogueGame) { g.moveHero(-1, 0) },
|
||||
'l': func(g *RogueGame) { g.moveHero(0, 1) },
|
||||
'y': func(g *RogueGame) { g.moveHero(-1, -1) },
|
||||
'u': func(g *RogueGame) { g.moveHero(-1, 1) },
|
||||
'b': func(g *RogueGame) { g.moveHero(1, -1) },
|
||||
'n': func(g *RogueGame) { g.moveHero(1, 1) },
|
||||
'H': func(g *RogueGame) { g.startRun('h') },
|
||||
'J': func(g *RogueGame) { g.startRun('j') },
|
||||
'K': func(g *RogueGame) { g.startRun('k') },
|
||||
'L': func(g *RogueGame) { g.startRun('l') },
|
||||
'Y': func(g *RogueGame) { g.startRun('y') },
|
||||
'U': func(g *RogueGame) { g.startRun('u') },
|
||||
'B': func(g *RogueGame) { g.startRun('b') },
|
||||
'N': func(g *RogueGame) { g.startRun('n') },
|
||||
't': func(g *RogueGame) {
|
||||
if !g.promptDirection() {
|
||||
g.After = false
|
||||
} else {
|
||||
g.missile(g.Delta.Y, g.Delta.X)
|
||||
}
|
||||
},
|
||||
'q': (*RogueGame).quaff,
|
||||
'Q': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.QComm = true
|
||||
g.quit(0)
|
||||
g.QComm = false
|
||||
},
|
||||
'i': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.inventory(g.Player.Pack, 0)
|
||||
},
|
||||
'I': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.pickyInven()
|
||||
},
|
||||
'd': (*RogueGame).dropIt,
|
||||
'r': (*RogueGame).readScroll,
|
||||
'e': (*RogueGame).eat,
|
||||
'w': (*RogueGame).wield,
|
||||
'W': (*RogueGame).wear,
|
||||
'T': (*RogueGame).takeOff,
|
||||
'P': (*RogueGame).ringOn,
|
||||
'R': (*RogueGame).ringOff,
|
||||
'o': func(g *RogueGame) {
|
||||
g.option()
|
||||
g.After = false
|
||||
},
|
||||
'c': func(g *RogueGame) {
|
||||
g.call()
|
||||
g.After = false
|
||||
},
|
||||
'>': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.dLevel()
|
||||
},
|
||||
'<': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.uLevel()
|
||||
},
|
||||
'?': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.help()
|
||||
},
|
||||
'/': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.identify()
|
||||
},
|
||||
's': (*RogueGame).search,
|
||||
'z': func(g *RogueGame) {
|
||||
if g.promptDirection() {
|
||||
g.doZap()
|
||||
} else {
|
||||
g.After = false
|
||||
}
|
||||
},
|
||||
'D': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.discovered()
|
||||
},
|
||||
CTRL('P'): func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.msg("%s", g.Msgs.Huh)
|
||||
},
|
||||
CTRL('R'): func(g *RogueGame) {
|
||||
g.After = false
|
||||
// C forces a full repaint here (clearok + wrefresh on
|
||||
// curscr), not the diffing refresh: the command exists
|
||||
// for screens the game's own record no longer matches.
|
||||
g.repaint()
|
||||
},
|
||||
'v': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.msg("version %s. (mctesq was here)", Release)
|
||||
},
|
||||
'S': func(g *RogueGame) {
|
||||
g.After = false
|
||||
g.saveGame()
|
||||
},
|
||||
'.': func(*RogueGame) {
|
||||
// rest command
|
||||
},
|
||||
' ': func(g *RogueGame) {
|
||||
g.After = false // "legal" illegal command
|
||||
},
|
||||
'^': (*RogueGame).identifyTrapCommand,
|
||||
'+': (*RogueGame).wizardToggleCommand,
|
||||
Escape: func(g *RogueGame) {
|
||||
g.DoorStop = false
|
||||
g.Count = 0
|
||||
g.After = false
|
||||
g.Again = false
|
||||
},
|
||||
')': func(g *RogueGame) {
|
||||
g.current(g.Player.CurWeapon, "wielding", "")
|
||||
},
|
||||
']': func(g *RogueGame) {
|
||||
g.current(g.Player.CurArmor, "wearing", "")
|
||||
},
|
||||
'=': func(g *RogueGame) {
|
||||
g.current(g.Player.CurRing[Left], "wearing",
|
||||
g.chooseTerse("(L)", "on left hand"))
|
||||
g.current(g.Player.CurRing[Right], "wearing",
|
||||
g.chooseTerse("(R)", "on right hand"))
|
||||
},
|
||||
'@': func(g *RogueGame) {
|
||||
g.StatMsg = true
|
||||
g.status()
|
||||
g.StatMsg = false
|
||||
g.After = false
|
||||
},
|
||||
},
|
||||
|
||||
trapHandlers: [NumTrapTypes]func(*RogueGame, Coord){
|
||||
TrapDoor: (*RogueGame).trapFall,
|
||||
TrapArrow: (*RogueGame).trapArrow,
|
||||
TrapSleep: (*RogueGame).trapSleep,
|
||||
TrapBear: (*RogueGame).trapBear,
|
||||
TrapTeleport: (*RogueGame).trapTeleport,
|
||||
TrapDart: (*RogueGame).trapDart,
|
||||
TrapRust: (*RogueGame).trapRust,
|
||||
TrapMystery: (*RogueGame).trapMystery,
|
||||
},
|
||||
|
||||
daemonHandlers: [DTurnSee + 1]func(*RogueGame, int){
|
||||
DRollwand: (*RogueGame).rollwand,
|
||||
DDoctor: (*RogueGame).doctor,
|
||||
DStomach: (*RogueGame).stomach,
|
||||
DRunners: (*RogueGame).runners,
|
||||
DSwander: (*RogueGame).swander,
|
||||
DNohaste: (*RogueGame).nohaste,
|
||||
DUnconfuse: (*RogueGame).unconfuse,
|
||||
DUnsee: (*RogueGame).unsee,
|
||||
DSight: (*RogueGame).sight,
|
||||
DVisuals: (*RogueGame).visuals,
|
||||
DComeDown: (*RogueGame).comeDown,
|
||||
DLand: (*RogueGame).land,
|
||||
DTurnSee: func(g *RogueGame, arg int) {
|
||||
g.turnSee(arg != 0)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// The three effect-table lookups below are the guarded form of a raw
|
||||
// index into quaffHandlers/readHandlers/zapHandlers. An object whose
|
||||
// Which is out of range for its kind reports no handler rather than
|
||||
// panicking, which lands on the same do-nothing behavior C's switches
|
||||
// had when no case matched.
|
||||
|
||||
// quaffHandler returns the potion effect for obj, or nil when obj is not
|
||||
// a potion the table knows about.
|
||||
func (d *gameData) quaffHandler(obj *Object) func(g *RogueGame, trip bool) {
|
||||
if !obj.hasValidWhich() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.quaffHandlers[obj.PotionKind()]
|
||||
}
|
||||
|
||||
// readHandler returns the scroll effect for obj, or nil when obj is not
|
||||
// a scroll the table knows about.
|
||||
func (d *gameData) readHandler(obj *Object) func(g *RogueGame, obj *Object) {
|
||||
if !obj.hasValidWhich() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.readHandlers[obj.ScrollKind()]
|
||||
}
|
||||
|
||||
// zapHandler returns the wand effect for obj, or nil when obj is not a
|
||||
// wand the table knows about.
|
||||
func (d *gameData) zapHandler(obj *Object) func(g *RogueGame, obj *Object) bool {
|
||||
if !obj.hasValidWhich() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.zapHandlers[obj.WandKind()]
|
||||
}
|
||||
|
||||
// identifyType reads extern.c's identify-scroll to item-kind map,
|
||||
// returning KindNone for any scroll past the last identify scroll (the
|
||||
// table is shorter than the scroll table it is keyed by). No scroll that
|
||||
// can reach readIdentify is past that end, so the guard is defensive
|
||||
// rather than a live bound; it exists so a future table resize cannot
|
||||
// turn the lookup into a panic.
|
||||
func (d *gameData) identifyType(kind ScrollKind) ObjectKind {
|
||||
if kind < 0 || int(kind) >= len(d.idType) {
|
||||
return KindNone
|
||||
}
|
||||
|
||||
return d.idType[kind]
|
||||
}
|
||||
|
||||
// armorClass reads extern.c a_class[] for an armor object's Which,
|
||||
// returning 0 when Which is out of range. Only a malformed object can
|
||||
// hit that arm — createObj and Restore both reject one — but a bounds
|
||||
// check here is what keeps a bad index from panicking the process
|
||||
// instead of merely naming a suit of armor oddly.
|
||||
func (d *gameData) armorClass(which int) int {
|
||||
if which < 0 || which >= int(NumArmorTypes) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return d.aClass[which]
|
||||
}
|
||||
|
||||
// Version strings (vers.c). The encstr/statlist XOR keys are not ported:
|
||||
// the Go save format does not use them.
|
||||
const (
|
||||
Release = "5.4.4"
|
||||
Version = "rogue (git.eeqj.de/sneak/rgoue port of rogueforge 5.4.4)"
|
||||
)
|
||||
@@ -1,132 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
// badcheck from init.c: every probability table must sum to exactly 100.
|
||||
func TestProbabilitiesSumTo100(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sum := func(info []ObjInfo) int {
|
||||
s := 0
|
||||
for _, oi := range info {
|
||||
s += oi.Prob
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
data := newGameData()
|
||||
|
||||
tables := map[string][]ObjInfo{
|
||||
"things": data.baseThings[:],
|
||||
"potions": data.basePotInfo[:],
|
||||
"scrolls": data.baseScrInfo[:],
|
||||
"rings": data.baseRingInfo[:],
|
||||
"sticks": data.baseWsInfo[:],
|
||||
"weapons": data.baseWeapInfo[:NumWeaponTypes], // excludes the flame entry
|
||||
"armor": data.baseArmInfo[:],
|
||||
}
|
||||
for name, tab := range tables {
|
||||
if s := sum(tab); s != 100 {
|
||||
t.Errorf("bad percentages for %s: sum = %d, want 100", name, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitProbsCumulative(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := New(Params{Seed: 1})
|
||||
|
||||
last := g.Items.Potions[NumPotionTypes-1].Prob
|
||||
if last != 100 {
|
||||
t.Errorf("cumulative potion probability ends at %d, want 100", last)
|
||||
}
|
||||
|
||||
for i := PotionKind(1); i < NumPotionTypes; i++ {
|
||||
if g.Items.Potions[i].Prob < g.Items.Potions[i-1].Prob {
|
||||
t.Errorf("potion probs not nondecreasing at %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGameRandomizesAppearances(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := New(Params{Seed: 12345})
|
||||
|
||||
checkPotionColors(t, g)
|
||||
checkScrollNames(t, g)
|
||||
checkWandMaterials(t, g)
|
||||
|
||||
// Determinism: same seed, same appearances.
|
||||
h := New(Params{Seed: 12345})
|
||||
if h.Items != g.Items {
|
||||
t.Error("two games with the same seed produced different item lore")
|
||||
}
|
||||
}
|
||||
|
||||
// checkPotionColors verifies every potion has a distinct color.
|
||||
func checkPotionColors(t *testing.T, g *RogueGame) {
|
||||
t.Helper()
|
||||
|
||||
seen := map[string]bool{}
|
||||
|
||||
for i, c := range g.Items.PotColors {
|
||||
if c == "" {
|
||||
t.Fatalf("potion %d has no color", i)
|
||||
}
|
||||
|
||||
if seen[c] {
|
||||
t.Errorf("potion color %q assigned twice", c)
|
||||
}
|
||||
|
||||
seen[c] = true
|
||||
}
|
||||
}
|
||||
|
||||
// checkScrollNames verifies every scroll has a name within the C buffer
|
||||
// limit.
|
||||
func checkScrollNames(t *testing.T, g *RogueGame) {
|
||||
t.Helper()
|
||||
|
||||
for i, n := range g.Items.ScrNames {
|
||||
if n == "" {
|
||||
t.Fatalf("scroll %d has no name", i)
|
||||
}
|
||||
|
||||
if len(n) > MaxNameLen+1 {
|
||||
t.Errorf("scroll name %q longer than C buffer allows", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkWandMaterials verifies every stick has a wand/staff type and a
|
||||
// material.
|
||||
func checkWandMaterials(t *testing.T, g *RogueGame) {
|
||||
t.Helper()
|
||||
|
||||
for i := range g.Items.WandType {
|
||||
if g.Items.WandType[i] != wandName && g.Items.WandType[i] != staffName {
|
||||
t.Errorf("stick %d has type %q", i, g.Items.WandType[i])
|
||||
}
|
||||
|
||||
if g.Items.WandMade[i] == "" {
|
||||
t.Errorf("stick %d has no material", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonsterTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := newGameData()
|
||||
if data.monsterTable[0].Name != "aquator" || data.monsterTable[25].Name != "zombie" {
|
||||
t.Error("monster table order broken")
|
||||
}
|
||||
|
||||
if data.monsterTable['D'-'A'].Name != "dragon" {
|
||||
t.Error("letter indexing broken")
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// testTerm is a headless Terminal for tests: rendering is a no-op and
|
||||
// input plays a script, then alternates space/newline so that --More--
|
||||
// and [Press return] prompts never block.
|
||||
type testTerm struct {
|
||||
input []byte
|
||||
pos int
|
||||
tick int
|
||||
// repaints counts forced full redraws. Rendering is a no-op here, so
|
||||
// counting is the only way a headless test can tell that CTRL-R asked
|
||||
// for a repaint rather than an ordinary refresh — the two are
|
||||
// indistinguishable in the window contents, which is the whole reason
|
||||
// the bug this replaces went unnoticed.
|
||||
repaints int
|
||||
}
|
||||
|
||||
func (t *testTerm) Render(*Window) {}
|
||||
|
||||
func (t *testTerm) Repaint() { t.repaints++ }
|
||||
|
||||
func (t *testTerm) Fini() {}
|
||||
|
||||
// Interrupt has nothing to wake: this terminal's ReadChar never blocks.
|
||||
// The blocking case has its own fake, blockingTerm in autosave_test.go.
|
||||
func (t *testTerm) Interrupt() {}
|
||||
|
||||
func (t *testTerm) ReadChar() (byte, bool) {
|
||||
if t.pos < len(t.input) {
|
||||
c := t.input[t.pos]
|
||||
t.pos++
|
||||
|
||||
return c, true
|
||||
}
|
||||
|
||||
t.tick++
|
||||
if t.tick%2 == 0 {
|
||||
return '\n', true
|
||||
}
|
||||
|
||||
return ' ', true
|
||||
}
|
||||
27
game/testdata/README.md
vendored
27
game/testdata/README.md
vendored
@@ -1,27 +0,0 @@
|
||||
# Seed-compatibility golden
|
||||
|
||||
`item_tables.golden` is the per-seed item appearance tables (potion colors,
|
||||
scroll names, ring stones, wand/staff materials) captured from the **C
|
||||
reference** on the `modern-rogue` branch, for the seeds in the `seeds` list in
|
||||
`TestSeedCompatItemTables`. That test regenerates the same tables from the Go
|
||||
port and checks they match byte for byte — proving the LCG and its consumption
|
||||
order through the whole init sequence (`init_probs` → `init_player` →
|
||||
`init_names` → `init_colors` → `init_stones` → `init_materials`) agree with C.
|
||||
|
||||
## Regenerating the golden
|
||||
|
||||
`c_seedcompat.patch` adds a `DUMP` mode to the C `main.c`: with `DUMP` set it
|
||||
forces the RNG seed from `SEED`, runs the item-table init in the normal order,
|
||||
prints the tables, and exits before `initscr` (so no terminal is needed).
|
||||
|
||||
```sh
|
||||
# from a checkout of the C reference (modern-rogue branch):
|
||||
git archive modern-rogue | tar -x -C /tmp/rogue-c
|
||||
cd /tmp/rogue-c
|
||||
patch -p1 < .../game/testdata/c_seedcompat.patch
|
||||
./configure && make
|
||||
for s in 1 42 12345 99999; do DUMP=1 SEED=$s ./rogue; done \
|
||||
> .../game/testdata/item_tables.golden
|
||||
```
|
||||
|
||||
The seed list must match the `seeds` slice in `TestSeedCompatItemTables`.
|
||||
37
game/testdata/c_seedcompat.patch
vendored
37
game/testdata/c_seedcompat.patch
vendored
@@ -1,37 +0,0 @@
|
||||
--- a/main.c 2026-07-24 03:02:38
|
||||
+++ b/main.c 2026-07-24 02:48:31
|
||||
@@ -63,6 +63,34 @@
|
||||
#endif
|
||||
dnum = lowtime + md_getpid();
|
||||
seed = dnum;
|
||||
+
|
||||
+ /* SEEDCOMPAT: dump the per-game item appearance tables for a fixed
|
||||
+ * seed and exit, without initscr. The init sequence and everything
|
||||
+ * it consumes from rnd() mirror the normal startup (main.c), so the
|
||||
+ * tables are exactly what a real game with SEED would show. */
|
||||
+ if (getenv("DUMP") != NULL)
|
||||
+ {
|
||||
+ int di;
|
||||
+ char *sv = getenv("SEED");
|
||||
+ if (sv != NULL)
|
||||
+ seed = atoi(sv);
|
||||
+ printf("SEED %d\n", seed);
|
||||
+ init_probs();
|
||||
+ init_player();
|
||||
+ init_names();
|
||||
+ init_colors();
|
||||
+ init_stones();
|
||||
+ init_materials();
|
||||
+ printf("POTIONS\n");
|
||||
+ for (di = 0; di < MAXPOTIONS; di++) printf("%s\n", p_colors[di]);
|
||||
+ printf("SCROLLS\n");
|
||||
+ for (di = 0; di < MAXSCROLLS; di++) printf("%s\n", s_names[di]);
|
||||
+ printf("RINGS\n");
|
||||
+ for (di = 0; di < MAXRINGS; di++) printf("%s\n", r_stones[di]);
|
||||
+ printf("STICKS\n");
|
||||
+ for (di = 0; di < MAXSTICKS; di++) printf("%s %s\n", ws_type[di], ws_made[di]);
|
||||
+ exit(0);
|
||||
+ }
|
||||
|
||||
open_score();
|
||||
|
||||
260
game/testdata/item_tables.golden
vendored
260
game/testdata/item_tables.golden
vendored
@@ -1,260 +0,0 @@
|
||||
SEED 1
|
||||
POTIONS
|
||||
tangerine
|
||||
white
|
||||
ecru
|
||||
gold
|
||||
amber
|
||||
violet
|
||||
vermilion
|
||||
pink
|
||||
aquamarine
|
||||
plaid
|
||||
clear
|
||||
orange
|
||||
cyan
|
||||
tan
|
||||
SCROLLS
|
||||
miwhon garsnanih
|
||||
xomimi roke eshwedshu
|
||||
potwexrol ipbjorod turs evsnelg
|
||||
bekornan oxyfatox
|
||||
iv wexpo wun
|
||||
ha sefnelgtue whon pay
|
||||
alari wedit
|
||||
zantmon umzonski umwhonjo yot
|
||||
bluoxun rokkho yottrol sta
|
||||
vomarg microgcomp iteulkshu mung
|
||||
jo urokeep yuskiun
|
||||
ox xozantaks klisstaevs ag
|
||||
ipnih bek
|
||||
shu ami erk
|
||||
nejti zim
|
||||
iprol mic ishoxyvom fagan
|
||||
reacreti oodrol
|
||||
bytsri solsa tabu fri
|
||||
RINGS
|
||||
agate
|
||||
zircon
|
||||
jade
|
||||
tiger eye
|
||||
onyx
|
||||
germanium
|
||||
lapis lazuli
|
||||
emerald
|
||||
taaffeite
|
||||
kryptonite
|
||||
garnet
|
||||
ruby
|
||||
turquoise
|
||||
pearl
|
||||
STICKS
|
||||
wand steel
|
||||
wand platinum
|
||||
staff redwood
|
||||
staff pine
|
||||
wand silicon
|
||||
staff spruce
|
||||
staff pecan
|
||||
wand bone
|
||||
staff maple
|
||||
wand zinc
|
||||
wand iron
|
||||
wand pewter
|
||||
wand electrum
|
||||
staff dogwood
|
||||
SEED 42
|
||||
POTIONS
|
||||
blue
|
||||
green
|
||||
grey
|
||||
amber
|
||||
violet
|
||||
gold
|
||||
pink
|
||||
tan
|
||||
purple
|
||||
yellow
|
||||
plaid
|
||||
magenta
|
||||
turquoise
|
||||
cyan
|
||||
SCROLLS
|
||||
bek itod oxytaod oxy
|
||||
tarhovzant cre sname oxroy
|
||||
plemik ganod hyd wergerkpot
|
||||
hyd sol um bekzok
|
||||
esh eep ganmung
|
||||
anera ishsa ingala mon
|
||||
alasniklech viv
|
||||
yunejorn garro con nej
|
||||
dotrolther gopum eltitrol trolmonsri
|
||||
nes alazum
|
||||
itegopmung ti
|
||||
ere haeta wergla
|
||||
nejerecre poipi iprea
|
||||
ha falechrhov
|
||||
monskiwex sabitla frido
|
||||
rhovmar sno
|
||||
mar bekurzant satbuzum
|
||||
somon sri
|
||||
RINGS
|
||||
carnelian
|
||||
onyx
|
||||
jade
|
||||
granite
|
||||
stibotantalite
|
||||
kryptonite
|
||||
lapis lazuli
|
||||
germanium
|
||||
garnet
|
||||
tiger eye
|
||||
opal
|
||||
topaz
|
||||
agate
|
||||
peridot
|
||||
STICKS
|
||||
staff birch
|
||||
staff ebony
|
||||
staff redwood
|
||||
wand gold
|
||||
wand copper
|
||||
wand aluminum
|
||||
wand titanium
|
||||
wand mercury
|
||||
staff cypress
|
||||
staff bamboo
|
||||
staff dogwood
|
||||
wand silicon
|
||||
staff zebrawood
|
||||
wand beryllium
|
||||
SEED 12345
|
||||
POTIONS
|
||||
purple
|
||||
black
|
||||
grey
|
||||
brown
|
||||
plaid
|
||||
violet
|
||||
vermilion
|
||||
ecru
|
||||
orange
|
||||
turquoise
|
||||
tan
|
||||
magenta
|
||||
silver
|
||||
gold
|
||||
SCROLLS
|
||||
readalf shuplu ivnin
|
||||
plelaiv solel skibyt monha
|
||||
xo wun
|
||||
wedyfri o ewhonxo favompay
|
||||
eep zantreanelg
|
||||
plu buxo
|
||||
un zontabdan
|
||||
bie snik
|
||||
ulkitzant bluri
|
||||
apporg ash posnevly dennepwex
|
||||
u urval rol
|
||||
arzepotsno snovly pay snoropay
|
||||
pottox erewed faoxro
|
||||
ther sun ulkipo mik
|
||||
argzebfri elgrekli tuenepzon sehturssef
|
||||
isheep blumur
|
||||
wedash yuzimsun
|
||||
plupofri ski rejo fa
|
||||
RINGS
|
||||
onyx
|
||||
tiger eye
|
||||
alexandrite
|
||||
turquoise
|
||||
pearl
|
||||
emerald
|
||||
germanium
|
||||
sapphire
|
||||
zircon
|
||||
ruby
|
||||
granite
|
||||
stibotantalite
|
||||
opal
|
||||
diamond
|
||||
STICKS
|
||||
wand silicon
|
||||
staff ironwood
|
||||
staff holly
|
||||
wand gold
|
||||
staff mahogany
|
||||
wand iron
|
||||
wand brass
|
||||
wand pewter
|
||||
staff hemlock
|
||||
staff cherry
|
||||
staff elm
|
||||
wand mercury
|
||||
staff banyan
|
||||
staff dogwood
|
||||
SEED 99999
|
||||
POTIONS
|
||||
aquamarine
|
||||
plaid
|
||||
gold
|
||||
black
|
||||
vermilion
|
||||
red
|
||||
cyan
|
||||
tan
|
||||
orange
|
||||
violet
|
||||
brown
|
||||
clear
|
||||
silver
|
||||
green
|
||||
SCROLLS
|
||||
zant jocompan vomervly
|
||||
mur shusat prok
|
||||
prokmurklis oxysriklis
|
||||
ingcre prokbu whonengarg kli
|
||||
kli bot
|
||||
rokcoswerg ipsolsan klisvlypay
|
||||
glen yot whontox
|
||||
lechme markho fazim
|
||||
dalfsunbie micjosef cre comp
|
||||
vlyfumi bjorzantbot werg
|
||||
po argfidcos klipones
|
||||
ashtemarg ycrezim dalfiv whon
|
||||
turs unmisa zimpo therdo
|
||||
miccompuni uni neswex sef
|
||||
odwexing elwergmur mung
|
||||
itcon rhov nejmic lech
|
||||
garturs engseh ganish
|
||||
oodta whonorgsno monabmik vomyeng
|
||||
RINGS
|
||||
obsidian
|
||||
moonstone
|
||||
jade
|
||||
carnelian
|
||||
tiger eye
|
||||
taaffeite
|
||||
turquoise
|
||||
stibotantalite
|
||||
agate
|
||||
ruby
|
||||
onyx
|
||||
topaz
|
||||
germanium
|
||||
granite
|
||||
STICKS
|
||||
wand titanium
|
||||
wand brass
|
||||
wand silicon
|
||||
staff zebrawood
|
||||
wand mercury
|
||||
staff dogwood
|
||||
wand pewter
|
||||
staff cinnibar
|
||||
staff kukui wood
|
||||
staff banyan
|
||||
wand magnesium
|
||||
wand gold
|
||||
staff maple
|
||||
wand nickel
|
||||
746
game/things.go
746
game/things.go
@@ -1,746 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// things.c — item naming, random item creation, and the discovery list.
|
||||
|
||||
// invName returns the name of something as it would appear in an inventory
|
||||
// (things.c inv_name).
|
||||
func (g *RogueGame) inventoryName(obj *Object, drop bool) string {
|
||||
var pb strings.Builder
|
||||
|
||||
// Every arm below reaches into a per-kind name table at obj.Which:
|
||||
// the potion colors, ring stones, wand material/type, scroll titles,
|
||||
// and the weapon and armor name tables. An object with an out-of-range
|
||||
// Which cannot reach here (createObj and Restore both reject one), but
|
||||
// if one ever did, naming it must not take the process down — so it
|
||||
// falls back to the bare category name from C's type_name() vocabulary.
|
||||
if !obj.hasValidWhich() {
|
||||
return obj.Kind.String()
|
||||
}
|
||||
|
||||
which := obj.Which
|
||||
it := &g.Items
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.Kind {
|
||||
case KindPotion:
|
||||
g.nameit(&pb, obj, potionName, it.PotColors[which], &it.Potions[which], nullstr)
|
||||
case KindRing:
|
||||
g.nameit(&pb, obj, ringName, it.RingStones[which], &it.Rings[which], ringNum)
|
||||
case KindWand:
|
||||
g.nameit(&pb, obj, it.WandType[which], it.WandMade[which],
|
||||
&it.Sticks[which], chargeStr)
|
||||
case KindScroll:
|
||||
g.nameScroll(&pb, obj)
|
||||
case KindFood:
|
||||
g.nameFood(&pb, obj)
|
||||
case KindWeapon:
|
||||
g.nameWeapon(&pb, obj)
|
||||
case KindArmor:
|
||||
g.nameArmor(&pb, obj)
|
||||
case KindAmulet:
|
||||
pb.WriteString("The Amulet of Yendor")
|
||||
case KindGold:
|
||||
fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldValue)
|
||||
}
|
||||
|
||||
return fixNameCase(g.describeWorn(obj, pb.String()), drop)
|
||||
}
|
||||
|
||||
// nameScroll writes a scroll's inventory name (things.c inv_name).
|
||||
func (g *RogueGame) nameScroll(pb *strings.Builder, obj *Object) {
|
||||
if obj.Count == 1 {
|
||||
pb.WriteString("A scroll ")
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d scrolls ", obj.Count)
|
||||
}
|
||||
|
||||
op := &g.Items.Scrolls[obj.Which]
|
||||
|
||||
switch {
|
||||
case op.Know:
|
||||
fmt.Fprintf(pb, "of %s", op.Name)
|
||||
case op.Guess != "":
|
||||
fmt.Fprintf(pb, "called %s", op.Guess)
|
||||
default:
|
||||
fmt.Fprintf(pb, "titled '%s'", g.Items.ScrNames[obj.Which])
|
||||
}
|
||||
}
|
||||
|
||||
// nameFood writes a food item's inventory name; which 1 is the fruit
|
||||
// (things.c inv_name).
|
||||
func (g *RogueGame) nameFood(pb *strings.Builder, obj *Object) {
|
||||
if obj.Which == 1 {
|
||||
if obj.Count == 1 {
|
||||
fmt.Fprintf(pb, "A%s %s", vowelstr(g.Fruit), g.Fruit)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d %ss", obj.Count, g.Fruit)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Count == 1 {
|
||||
pb.WriteString("Some food")
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d rations of food", obj.Count)
|
||||
}
|
||||
}
|
||||
|
||||
// nameWeapon writes a weapon's inventory name (things.c inv_name).
|
||||
func (g *RogueGame) nameWeapon(pb *strings.Builder, obj *Object) {
|
||||
sp := g.Items.Weapons[obj.Which].Name
|
||||
|
||||
if obj.Count > 1 {
|
||||
fmt.Fprintf(pb, "%d ", obj.Count)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "A%s ", vowelstr(sp))
|
||||
}
|
||||
|
||||
if obj.Flags.Has(Known) {
|
||||
fmt.Fprintf(pb, "%s %s", num(obj.HPlus, obj.DPlus, Weapon), sp)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
|
||||
if obj.Count > 1 {
|
||||
pb.WriteString("s")
|
||||
}
|
||||
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(pb, " called %s", obj.Label)
|
||||
}
|
||||
}
|
||||
|
||||
// nameArmor writes an armor's inventory name (things.c inv_name).
|
||||
func (g *RogueGame) nameArmor(pb *strings.Builder, obj *Object) {
|
||||
sp := g.Items.Armors[obj.Which].Name
|
||||
if obj.Flags.Has(Known) {
|
||||
fmt.Fprintf(pb, "%s %s [",
|
||||
num(g.data.armorClass(obj.Which)-obj.ArmorClass, 0, Armor), sp)
|
||||
|
||||
if !g.Options.Terse {
|
||||
pb.WriteString("protection ")
|
||||
}
|
||||
|
||||
fmt.Fprintf(pb, "%d]", 10-obj.ArmorClass)
|
||||
} else {
|
||||
pb.WriteString(sp)
|
||||
}
|
||||
|
||||
if obj.Label != "" {
|
||||
fmt.Fprintf(pb, " called %s", obj.Label)
|
||||
}
|
||||
}
|
||||
|
||||
// describeWorn appends the equipped-status notes to an inventory name
|
||||
// (things.c inv_name).
|
||||
func (g *RogueGame) describeWorn(obj *Object, out string) string {
|
||||
if !g.InvDescribe {
|
||||
return out
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
if obj == p.CurArmor {
|
||||
out += " (being worn)"
|
||||
}
|
||||
|
||||
if obj == p.CurWeapon {
|
||||
out += " (weapon in hand)"
|
||||
}
|
||||
|
||||
switch obj {
|
||||
case p.CurRing[Left]:
|
||||
out += " (on left hand)"
|
||||
case p.CurRing[Right]:
|
||||
out += " (on right hand)"
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// fixNameCase upper- or lowercases the leading letter to suit the
|
||||
// sentence it will land in (things.c inv_name).
|
||||
func fixNameCase(out string, drop bool) string {
|
||||
if out == "" {
|
||||
return out
|
||||
}
|
||||
|
||||
if drop && isUpper(out[0]) {
|
||||
return string(toLower(out[0])) + out[1:]
|
||||
}
|
||||
|
||||
if !drop && isLower(out[0]) {
|
||||
return string(toUpper(out[0])) + out[1:]
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// dropIt puts something down (things.c drop; renamed to avoid the
|
||||
// leavePack/detach vocabulary collision).
|
||||
func (g *RogueGame) dropIt() {
|
||||
p := &g.Player
|
||||
|
||||
ch := g.Level.Char(p.Pos.Y, p.Pos.X)
|
||||
if ch != Floor && ch != Passage {
|
||||
g.After = false
|
||||
g.msg("there is something there already")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
obj, ok := g.promptPackItem("drop", KindNone)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if !g.dropCheck(obj) {
|
||||
return
|
||||
}
|
||||
|
||||
obj = g.leavePack(obj, true, !obj.Kind.MergesInPack())
|
||||
// Link it into the level object list
|
||||
g.Level.AddObject(obj)
|
||||
g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Kind.Glyph())
|
||||
g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped)
|
||||
|
||||
obj.Pos = p.Pos
|
||||
if obj.Kind == KindAmulet {
|
||||
g.HasAmulet = false
|
||||
}
|
||||
|
||||
g.msg("dropped %s", g.inventoryName(obj, true))
|
||||
}
|
||||
|
||||
// dropCheck does special checks for dropping or unwielding|unwearing|
|
||||
// unringing (things.c dropcheck).
|
||||
func (g *RogueGame) dropCheck(obj *Object) bool {
|
||||
if obj == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
p := &g.Player
|
||||
if obj != p.CurArmor && obj != p.CurWeapon &&
|
||||
obj != p.CurRing[Left] && obj != p.CurRing[Right] {
|
||||
return true
|
||||
}
|
||||
|
||||
if obj.Flags.Has(Cursed) {
|
||||
g.msg("you can't. It appears to be cursed")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
switch obj {
|
||||
case p.CurWeapon:
|
||||
p.CurWeapon = nil
|
||||
case p.CurArmor:
|
||||
g.wasteTime()
|
||||
|
||||
p.CurArmor = nil
|
||||
default:
|
||||
g.dropRing(obj)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// dropRing takes a worn ring off with its side effects (things.c
|
||||
// dropcheck).
|
||||
func (g *RogueGame) dropRing(obj *Object) {
|
||||
p := &g.Player
|
||||
|
||||
hand := Right
|
||||
if obj == p.CurRing[Left] {
|
||||
hand = Left
|
||||
}
|
||||
|
||||
p.CurRing[hand] = nil
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.RingKind() {
|
||||
case RingAddStrength:
|
||||
g.changeStrength(-obj.Bonus)
|
||||
case RingSeeInvisible:
|
||||
g.unsee(0)
|
||||
g.Extinguish(DUnsee)
|
||||
}
|
||||
}
|
||||
|
||||
// newThing returns a new random thing for the dungeon (things.c new_thing).
|
||||
func (g *RogueGame) newThing() *Object {
|
||||
cur := newObject()
|
||||
cur.Damage = dice("0x0")
|
||||
cur.HurlDmg = dice("0x0")
|
||||
cur.ArmorClass = 11
|
||||
cur.Count = 1
|
||||
|
||||
// Decide what kind of object it will be; if we haven't had food for a
|
||||
// while, let it be food.
|
||||
var kind int
|
||||
if g.Player.NoFood > 3 {
|
||||
kind = 2
|
||||
} else {
|
||||
kind = pickOne(g, g.Items.Things[:])
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case 0:
|
||||
cur.Kind = KindPotion
|
||||
cur.Which = pickOne(g, g.Items.Potions[:])
|
||||
case 1:
|
||||
cur.Kind = KindScroll
|
||||
cur.Which = pickOne(g, g.Items.Scrolls[:])
|
||||
case 2:
|
||||
g.newFoodThing(cur)
|
||||
case 3:
|
||||
g.newWeaponThing(cur)
|
||||
case 4:
|
||||
g.newArmorThing(cur)
|
||||
case 5:
|
||||
g.newRingThing(cur)
|
||||
case 6:
|
||||
cur.Kind = KindWand
|
||||
cur.Which = pickOne(g, g.Items.Sticks[:])
|
||||
g.fixStick(cur)
|
||||
}
|
||||
|
||||
return cur
|
||||
}
|
||||
|
||||
// newFoodThing rolls food, one time in ten the fruit (things.c
|
||||
// new_thing).
|
||||
func (g *RogueGame) newFoodThing(cur *Object) {
|
||||
cur.Kind = KindFood
|
||||
|
||||
g.Player.NoFood = 0
|
||||
if g.rnd(10) != 0 {
|
||||
cur.Which = 0
|
||||
} else {
|
||||
cur.Which = 1
|
||||
}
|
||||
}
|
||||
|
||||
// newWeaponThing rolls a weapon, sometimes cursed or blessed (things.c
|
||||
// new_thing).
|
||||
func (g *RogueGame) newWeaponThing(cur *Object) {
|
||||
g.initWeapon(cur, WeaponKind(pickOne(g, g.Items.Weapons[:NumWeaponTypes])))
|
||||
|
||||
if r := g.rnd(100); r < 10 {
|
||||
cur.Flags.Set(Cursed)
|
||||
cur.HPlus -= g.rnd(3) + 1
|
||||
} else if r < 15 {
|
||||
cur.HPlus += g.rnd(3) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// newArmorThing rolls armor, sometimes cursed or blessed (things.c
|
||||
// new_thing).
|
||||
func (g *RogueGame) newArmorThing(cur *Object) {
|
||||
cur.Kind = KindArmor
|
||||
cur.Which = pickOne(g, g.Items.Armors[:])
|
||||
|
||||
cur.ArmorClass = g.data.aClass[cur.Which]
|
||||
if r := g.rnd(100); r < 20 {
|
||||
cur.Flags.Set(Cursed)
|
||||
cur.ArmorClass += g.rnd(3) + 1
|
||||
} else if r < 28 {
|
||||
cur.ArmorClass -= g.rnd(3) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// newRingThing rolls a ring, cursing the bad ones (things.c new_thing).
|
||||
func (g *RogueGame) newRingThing(cur *Object) {
|
||||
cur.Kind = KindRing
|
||||
|
||||
cur.Which = pickOne(g, g.Items.Rings[:])
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch cur.RingKind() {
|
||||
case RingAddStrength, RingProtection, RingDexterity, RingIncreaseDamage:
|
||||
if cur.Bonus = g.rnd(3); cur.Bonus == 0 {
|
||||
cur.Bonus = -1
|
||||
cur.Flags.Set(Cursed)
|
||||
}
|
||||
case RingAggravateMonsters, RingTeleportation:
|
||||
cur.Flags.Set(Cursed)
|
||||
}
|
||||
}
|
||||
|
||||
// pickOne picks an item out of a list of possible objects using their
|
||||
// cumulative probabilities (things.c pick_one).
|
||||
func pickOne(g *RogueGame, info []ObjInfo) int {
|
||||
i := g.rnd(100)
|
||||
for idx := range info {
|
||||
if i < info[idx].Prob {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
return 0 // bad pick_one: C resets to the start of the table
|
||||
}
|
||||
|
||||
// invPage is the things.c static pagination state for the discovery/
|
||||
// inventory list windows (line_cnt, newpage, lastfmt/lastarg, maxlen).
|
||||
type invPage struct {
|
||||
lineCnt int
|
||||
newpage bool
|
||||
lastLine string
|
||||
maxlen int
|
||||
init bool
|
||||
}
|
||||
|
||||
// discovered lists what the player has found of a certain type
|
||||
// (things.c discovered).
|
||||
func (g *RogueGame) discovered() {
|
||||
var ch byte
|
||||
|
||||
for {
|
||||
discList := false
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("for ")
|
||||
}
|
||||
|
||||
g.addmsgf("what type")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" of object do you want a list")
|
||||
}
|
||||
|
||||
g.msg("? (* for all)")
|
||||
|
||||
ch = g.readchar()
|
||||
switch ch {
|
||||
case Escape:
|
||||
g.msg("")
|
||||
|
||||
return
|
||||
case Potion, Scroll, Ring, Stick, '*':
|
||||
discList = true
|
||||
default:
|
||||
if g.Options.Terse {
|
||||
g.msg("Not a type")
|
||||
} else {
|
||||
g.msg("Please type one of %c%c%c%c (ESCAPE to quit)",
|
||||
Potion, Scroll, Ring, Stick)
|
||||
}
|
||||
}
|
||||
|
||||
if discList {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '*' {
|
||||
g.printDisc(Potion)
|
||||
g.addLine("")
|
||||
g.printDisc(Scroll)
|
||||
g.addLine("")
|
||||
g.printDisc(Ring)
|
||||
g.addLine("")
|
||||
g.printDisc(Stick)
|
||||
g.endLine()
|
||||
} else {
|
||||
g.printDisc(ch)
|
||||
g.endLine()
|
||||
}
|
||||
}
|
||||
|
||||
// printDisc prints what we've discovered of the given type
|
||||
// (things.c print_disc).
|
||||
func (g *RogueGame) printDisc(typ byte) {
|
||||
var info []ObjInfo
|
||||
|
||||
switch typ {
|
||||
case Scroll:
|
||||
info = g.Items.Scrolls[:]
|
||||
case Potion:
|
||||
info = g.Items.Potions[:]
|
||||
case Ring:
|
||||
info = g.Items.Rings[:]
|
||||
case Stick:
|
||||
info = g.Items.Sticks[:]
|
||||
}
|
||||
|
||||
order := make([]int, len(info))
|
||||
g.setOrder(order)
|
||||
|
||||
obj := Object{Count: 1}
|
||||
numFound := 0
|
||||
|
||||
for i := range info {
|
||||
if info[order[i]].Know || info[order[i]].Guess != "" {
|
||||
obj.Kind = objectKindForGlyph(typ)
|
||||
obj.Which = order[i]
|
||||
g.addLine("%s", g.inventoryName(&obj, false))
|
||||
|
||||
numFound++
|
||||
}
|
||||
}
|
||||
|
||||
if numFound == 0 {
|
||||
g.addLine("%s", g.nothing(typ))
|
||||
}
|
||||
}
|
||||
|
||||
// setOrder shuffles the display order for the discovery list
|
||||
// (things.c set_order).
|
||||
func (g *RogueGame) setOrder(order []int) {
|
||||
for i := range order {
|
||||
order[i] = i
|
||||
}
|
||||
|
||||
for i := len(order); i > 0; i-- {
|
||||
r := g.rnd(i)
|
||||
order[i-1], order[r] = order[r], order[i-1]
|
||||
}
|
||||
}
|
||||
|
||||
// addLine adds a line to the list of discoveries (things.c add_line). A
|
||||
// format of exactly "\x00" is the C fmt==NULL page-flush sentinel — use
|
||||
// flushLine() for that.
|
||||
const flushSentinel = "\x00"
|
||||
|
||||
func (g *RogueGame) addLine(format string, a ...any) int {
|
||||
pg := &g.invPage
|
||||
isFlush := format == flushSentinel
|
||||
|
||||
var line string
|
||||
if !isFlush {
|
||||
line = fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
if pg.lineCnt == 0 {
|
||||
g.scr.Hw.Clear()
|
||||
|
||||
if g.Options.InvType == InvSlow {
|
||||
g.Msgs.Mpos = 0
|
||||
}
|
||||
}
|
||||
|
||||
if g.Options.InvType == InvSlow {
|
||||
return g.addLineSlow(line, isFlush)
|
||||
}
|
||||
|
||||
g.addLinePaged(line, isFlush)
|
||||
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
// addLineSlow shows one discovery line as a message (the slow-inventory
|
||||
// arm of things.c add_line).
|
||||
func (g *RogueGame) addLineSlow(line string, isFlush bool) int {
|
||||
if !isFlush && line != "" {
|
||||
if g.msg("%s", line) == Escape {
|
||||
return Escape
|
||||
}
|
||||
}
|
||||
|
||||
g.invPage.lineCnt++
|
||||
|
||||
return ^Escape
|
||||
}
|
||||
|
||||
// addLinePaged accumulates discovery lines into the paged window,
|
||||
// prompting between full pages (the windowed arm of things.c add_line).
|
||||
func (g *RogueGame) addLinePaged(line string, isFlush bool) {
|
||||
pg := &g.invPage
|
||||
prompt := "--Press space to continue--"
|
||||
|
||||
if !pg.init {
|
||||
pg.maxlen = len(prompt)
|
||||
pg.init = true
|
||||
}
|
||||
|
||||
if pg.lineCnt >= NumLines-1 || isFlush {
|
||||
g.addLinePageBreak(prompt, isFlush)
|
||||
}
|
||||
|
||||
if !isFlush && (pg.lineCnt != 0 || line != "") {
|
||||
g.scr.Hw.MvAddStr(pg.lineCnt, 0, line)
|
||||
|
||||
pg.lineCnt++
|
||||
if pg.maxlen < len(line) {
|
||||
pg.maxlen = len(line)
|
||||
}
|
||||
|
||||
pg.lastLine = line
|
||||
}
|
||||
}
|
||||
|
||||
// addLinePageBreak prompts at a full page and starts a fresh one
|
||||
// (things.c add_line).
|
||||
func (g *RogueGame) addLinePageBreak(prompt string, isFlush bool) {
|
||||
pg := &g.invPage
|
||||
if g.Options.InvType == InvOver && isFlush && !pg.newpage {
|
||||
g.addLineOverlay(prompt)
|
||||
} else {
|
||||
g.scr.Hw.MvAddStr(NumLines-1, 0, prompt)
|
||||
g.scr.RefreshWin(g.scr.Hw)
|
||||
g.waitFor(' ')
|
||||
g.scr.Hw.Clear()
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
pg.newpage = true
|
||||
pg.lineCnt = 0
|
||||
pg.maxlen = len(prompt)
|
||||
}
|
||||
|
||||
// addLineOverlay draws the accumulated list in a box at the top right
|
||||
// of the screen, prompts, and restores what was beneath (things.c
|
||||
// add_line).
|
||||
func (g *RogueGame) addLineOverlay(prompt string) {
|
||||
pg := &g.invPage
|
||||
|
||||
g.msg("")
|
||||
g.refresh()
|
||||
|
||||
saved := NewWindow(NumLines, NumCols)
|
||||
saved.CopyFrom(g.scr.Std)
|
||||
|
||||
lx := NumCols - pg.maxlen - 2
|
||||
for y := 0; y <= pg.lineCnt; y++ {
|
||||
for x := 0; x <= pg.maxlen; x++ {
|
||||
g.scr.Std.MvAddCh(y, lx+x, g.scr.Hw.MvInch(y, x))
|
||||
}
|
||||
}
|
||||
|
||||
g.scr.Std.MvAddStr(pg.lineCnt, lx, prompt)
|
||||
g.refresh()
|
||||
g.waitFor(' ')
|
||||
g.scr.Std.CopyFrom(saved)
|
||||
g.refresh()
|
||||
}
|
||||
|
||||
// flushLine is add_line(NULL): force out the accumulated page.
|
||||
func (g *RogueGame) flushLine() int { return g.addLine(flushSentinel) }
|
||||
|
||||
// endLine ends the list of lines (things.c end_line).
|
||||
func (g *RogueGame) endLine() {
|
||||
pg := &g.invPage
|
||||
if g.Options.InvType != InvSlow {
|
||||
if pg.lineCnt == 1 && !pg.newpage {
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("%s", pg.lastLine)
|
||||
} else {
|
||||
g.flushLine()
|
||||
}
|
||||
}
|
||||
|
||||
pg.lineCnt = 0
|
||||
pg.newpage = false
|
||||
}
|
||||
|
||||
// nothing builds the "nothing found" message for a type (things.c nothing).
|
||||
func (g *RogueGame) nothing(typ byte) string {
|
||||
var out string
|
||||
if g.Options.Terse {
|
||||
out = "Nothing"
|
||||
} else {
|
||||
out = "Haven't discovered anything"
|
||||
}
|
||||
|
||||
if typ != '*' {
|
||||
var tystr string
|
||||
|
||||
switch typ {
|
||||
case Potion:
|
||||
tystr = potionName
|
||||
case Scroll:
|
||||
tystr = scrollName
|
||||
case Ring:
|
||||
tystr = ringName
|
||||
case Stick:
|
||||
tystr = "stick"
|
||||
}
|
||||
|
||||
out += fmt.Sprintf(" about any %ss", tystr)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// nameit gives the proper name to a potion, stick, or ring
|
||||
// (things.c nameit).
|
||||
func (g *RogueGame) nameit(pb *strings.Builder, obj *Object, typ, which string,
|
||||
op *ObjInfo, prfunc func(*RogueGame, *Object) string) {
|
||||
switch {
|
||||
case op.Know || op.Guess != "":
|
||||
if obj.Count == 1 {
|
||||
fmt.Fprintf(pb, "A %s ", typ)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "%d %ss ", obj.Count, typ)
|
||||
}
|
||||
|
||||
if op.Know {
|
||||
fmt.Fprintf(pb, "of %s%s(%s)", op.Name, prfunc(g, obj), which)
|
||||
} else {
|
||||
fmt.Fprintf(pb, "called %s%s(%s)", op.Guess, prfunc(g, obj), which)
|
||||
}
|
||||
case obj.Count == 1:
|
||||
fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ)
|
||||
default:
|
||||
fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ)
|
||||
}
|
||||
}
|
||||
|
||||
// nullstr returns an empty string (things.c nullstr).
|
||||
func nullstr(*RogueGame, *Object) string { return "" }
|
||||
|
||||
// prList lists possible potions, scrolls, etc. for the wizard (things.c
|
||||
// pr_list).
|
||||
func (g *RogueGame) prList() {
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("for ")
|
||||
}
|
||||
|
||||
g.addmsgf("what type")
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf(" of object do you want a list")
|
||||
}
|
||||
|
||||
g.msg("? ")
|
||||
|
||||
ch := g.readchar()
|
||||
switch ch {
|
||||
case Potion:
|
||||
g.prSpec(g.Items.Potions[:])
|
||||
case Scroll:
|
||||
g.prSpec(g.Items.Scrolls[:])
|
||||
case Ring:
|
||||
g.prSpec(g.Items.Rings[:])
|
||||
case Stick:
|
||||
g.prSpec(g.Items.Sticks[:])
|
||||
case Armor:
|
||||
g.prSpec(g.Items.Armors[:])
|
||||
case Weapon:
|
||||
g.prSpec(g.Items.Weapons[:NumWeaponTypes])
|
||||
}
|
||||
}
|
||||
|
||||
// prSpec prints a specific list of possible items to choose from
|
||||
// (things.c pr_spec).
|
||||
func (g *RogueGame) prSpec(info []ObjInfo) {
|
||||
lastprob := 0
|
||||
|
||||
i := byte('0')
|
||||
for idx := range info {
|
||||
if i == '9'+1 {
|
||||
i = 'a'
|
||||
}
|
||||
|
||||
g.addLine("%c: %s (%d%%)", i, info[idx].Name, info[idx].Prob-lastprob)
|
||||
lastprob = info[idx].Prob
|
||||
i++
|
||||
}
|
||||
|
||||
g.endLine()
|
||||
}
|
||||
418
game/types.go
418
game/types.go
@@ -1,418 +0,0 @@
|
||||
// Package game is a Go port of Rogue 5.4.4 ("Exploring the Dungeons of
|
||||
// Doom", Michael Toy, Ken Arnold and Glenn Wichman, 1980-1999).
|
||||
//
|
||||
// The port is function-by-function faithful to the C sources kept in the
|
||||
// repository root; ARCHITECTURE.md documents both the original program and
|
||||
// the design of this port. All formerly-global state lives on the RogueGame
|
||||
// type.
|
||||
package game
|
||||
|
||||
// Maximum number of different things (rogue.h)
|
||||
const (
|
||||
MaxRooms = 9
|
||||
MaxThings = 9
|
||||
MaxObj = 9
|
||||
MaxPack = 23
|
||||
MaxTraps = 10
|
||||
AmuletLevel = 26
|
||||
NumThings = 7 // number of types of things
|
||||
MaxPass = 13 // upper limit on number of passages
|
||||
NumLines = 24
|
||||
NumCols = 80
|
||||
StatLine = NumLines - 1
|
||||
BoreLevel = 50
|
||||
|
||||
MaxStr = 1024 // maximum length of strings
|
||||
MaxLines = 32 // maximum number of screen lines used
|
||||
MaxCols = 80 // maximum number of screen columns used
|
||||
)
|
||||
|
||||
// Return values for get functions (rogue.h)
|
||||
const (
|
||||
Norm = 0 // normal exit
|
||||
Quit = 1 // quit option setting
|
||||
Minus = 2 // back up one option
|
||||
)
|
||||
|
||||
// Inventory types (rogue.h)
|
||||
const (
|
||||
InvOver = 0
|
||||
InvSlow = 1
|
||||
InvClear = 2
|
||||
)
|
||||
|
||||
// Things that appear on the screens (rogue.h). These byte values serve both
|
||||
// as map characters and as Object.Type discriminators, exactly as in C.
|
||||
const (
|
||||
Passage byte = '#'
|
||||
Door byte = '+'
|
||||
Floor byte = '.'
|
||||
PlayerCh byte = '@'
|
||||
Trap byte = '^'
|
||||
Stairs byte = '%'
|
||||
Gold byte = '*'
|
||||
Potion byte = '!'
|
||||
Scroll byte = '?'
|
||||
Magic byte = '$'
|
||||
Food byte = ':'
|
||||
Weapon byte = ')'
|
||||
Armor byte = ']'
|
||||
Amulet byte = ','
|
||||
Ring byte = '='
|
||||
Stick byte = '/'
|
||||
)
|
||||
|
||||
// Various constants (rogue.h)
|
||||
const (
|
||||
HealTime = 30
|
||||
HuhDuration = 20
|
||||
SeeDuration = 850
|
||||
HungerTime = 1300
|
||||
MoreTime = 150
|
||||
StomachSize = 2000
|
||||
StarveTime = 850
|
||||
Escape = 27
|
||||
Left = 0
|
||||
Right = 1
|
||||
BoltLength = 6
|
||||
LampDist = 3
|
||||
MaxDaemons = 20
|
||||
MaxInp = 50 // options.c: max string entered by the user
|
||||
MaxNameLen = 40 // init.c MAXNAME: max chars in a generated scroll name
|
||||
)
|
||||
|
||||
// Save against things (rogue.h)
|
||||
const (
|
||||
VsPoison = 0
|
||||
VsParalyzation = 0
|
||||
VsDeath = 0
|
||||
VsBreath = 2
|
||||
VsMagic = 3
|
||||
)
|
||||
|
||||
// RoomFlags are the room state bits (rogue.h room flags).
|
||||
type RoomFlags int16
|
||||
|
||||
// Room state bits (rogue.h ISDARK/ISGONE/ISMAZE).
|
||||
const (
|
||||
Dark RoomFlags = 1 << iota // room is dark
|
||||
Gone // room is gone (a corridor)
|
||||
Maze // room is a maze
|
||||
)
|
||||
|
||||
// Has reports whether any of the given bits are set.
|
||||
func (f *RoomFlags) Has(b RoomFlags) bool { return *f&b != 0 }
|
||||
|
||||
// Set turns the given bits on.
|
||||
func (f *RoomFlags) Set(b RoomFlags) { *f |= b }
|
||||
|
||||
// Clear turns the given bits off.
|
||||
func (f *RoomFlags) Clear(b RoomFlags) { *f &^= b }
|
||||
|
||||
// ObjFlags are the object state bits (rogue.h object flags).
|
||||
type ObjFlags int32
|
||||
|
||||
// Object state bits (rogue.h).
|
||||
const (
|
||||
Cursed ObjFlags = 1 << iota // ISCURSED: object is cursed
|
||||
Known // ISKNOW: player knows details about the object
|
||||
Missile // ISMISL: object is a missile type
|
||||
Stackable // ISMANY: object comes in groups
|
||||
WasFound // ISFOUND (objects): seen; bit shared with creatures
|
||||
Protected // ISPROT: armor is permanently protected
|
||||
)
|
||||
|
||||
// Has reports whether any of the given bits are set.
|
||||
func (f *ObjFlags) Has(b ObjFlags) bool { return *f&b != 0 }
|
||||
|
||||
// Set turns the given bits on.
|
||||
func (f *ObjFlags) Set(b ObjFlags) { *f |= b }
|
||||
|
||||
// Clear turns the given bits off.
|
||||
func (f *ObjFlags) Clear(b ObjFlags) { *f &^= b }
|
||||
|
||||
// CreatureFlags are the creature state bits (rogue.h creature flags). The
|
||||
// C bit collisions are deliberate and preserved: one name of each pair
|
||||
// applies to monsters, the other to the hero, and they never coexist on
|
||||
// one creature.
|
||||
type CreatureFlags int32
|
||||
|
||||
// Creature state bits (rogue.h).
|
||||
const (
|
||||
CanConfuse CreatureFlags = 0o000001 // CANHUH: creature can confuse
|
||||
CanSeeInvisible CreatureFlags = 0o000002 // CANSEE: can see invisible creatures
|
||||
Blind CreatureFlags = 0o000004 // ISBLIND: creature is blind
|
||||
Cancelled CreatureFlags = 0o000010 // ISCANC: special qualities cancelled
|
||||
Levitating CreatureFlags = 0o000010 // ISLEVIT: hero is levitating
|
||||
Found CreatureFlags = 0o000020 // ISFOUND: creature has been seen
|
||||
Greedy CreatureFlags = 0o000040 // ISGREED: creature runs to protect gold
|
||||
Hasted CreatureFlags = 0o000100 // ISHASTE: creature has been hastened
|
||||
Targeted CreatureFlags = 0o000200 // ISTARGET: target of an 'f' command
|
||||
Held CreatureFlags = 0o000400 // ISHELD: creature has been held
|
||||
Confused CreatureFlags = 0o001000 // ISHUH: creature is confused
|
||||
Invisible CreatureFlags = 0o002000 // ISINVIS: creature is invisible
|
||||
Mean CreatureFlags = 0o004000 // ISMEAN: wakes when player enters room
|
||||
Hallucinating CreatureFlags = 0o004000 // ISHALU: hero is on acid trip
|
||||
Regenerates CreatureFlags = 0o010000 // ISREGEN: creature can regenerate
|
||||
Awake CreatureFlags = 0o020000 // ISRUN: creature is running at the player
|
||||
SenseMonsters CreatureFlags = 0o040000 // SEEMONST: hero can detect unseen monsters
|
||||
Flying CreatureFlags = 0o040000 // ISFLY: creature can fly
|
||||
Slowed CreatureFlags = 0o100000 // ISSLOW: creature has been slowed
|
||||
)
|
||||
|
||||
// Has reports whether any of the given bits are set.
|
||||
func (f *CreatureFlags) Has(b CreatureFlags) bool { return *f&b != 0 }
|
||||
|
||||
// Set turns the given bits on.
|
||||
func (f *CreatureFlags) Set(b CreatureFlags) { *f |= b }
|
||||
|
||||
// Clear turns the given bits off.
|
||||
func (f *CreatureFlags) Clear(b CreatureFlags) { *f &^= b }
|
||||
|
||||
// PlaceFlags are the per-map-cell bits (rogue.h level map flags). The low
|
||||
// bits double as the passage number (FPassNum) or trap kind (FTrapMask).
|
||||
type PlaceFlags uint8
|
||||
|
||||
// Map cell bits (rogue.h).
|
||||
const (
|
||||
FPassage PlaceFlags = 0x80 // F_PASS: is a passageway
|
||||
FSeen PlaceFlags = 0x40 // have seen this spot before
|
||||
FDropped PlaceFlags = 0x20 // object was dropped here
|
||||
FLocked PlaceFlags = 0x20 // door is locked
|
||||
FReal PlaceFlags = 0x10 // what you see is what you get
|
||||
FPassNum PlaceFlags = 0x0f // F_PNUM: passage number mask
|
||||
FTrapMask PlaceFlags = 0x07 // F_TMASK: trap number mask
|
||||
)
|
||||
|
||||
// Has reports whether any of the given bits are set.
|
||||
func (f *PlaceFlags) Has(b PlaceFlags) bool { return *f&b != 0 }
|
||||
|
||||
// Set turns the given bits on.
|
||||
func (f *PlaceFlags) Set(b PlaceFlags) { *f |= b }
|
||||
|
||||
// Clear turns the given bits off.
|
||||
func (f *PlaceFlags) Clear(b PlaceFlags) { *f &^= b }
|
||||
|
||||
// TrapKind identifies a trap (rogue.h trap types). The kind is stored in
|
||||
// the low bits of a map cell's PlaceFlags (FTrapMask).
|
||||
type TrapKind int
|
||||
|
||||
// Trap kinds (rogue.h T_* constants).
|
||||
const (
|
||||
TrapDoor TrapKind = 0
|
||||
TrapArrow TrapKind = 1
|
||||
TrapSleep TrapKind = 2
|
||||
TrapBear TrapKind = 3
|
||||
TrapTeleport TrapKind = 4
|
||||
TrapDart TrapKind = 5
|
||||
TrapRust TrapKind = 6
|
||||
TrapMystery TrapKind = 7
|
||||
NumTrapTypes = 8
|
||||
)
|
||||
|
||||
// PotionKind identifies a potion (rogue.h potion types).
|
||||
type PotionKind int
|
||||
|
||||
// Potion kinds (rogue.h)
|
||||
const (
|
||||
PotionConfusion PotionKind = iota
|
||||
PotionLSD
|
||||
PotionPoison
|
||||
PotionGainStrength
|
||||
PotionSeeInvisible
|
||||
PotionHealing
|
||||
PotionDetectMonsters
|
||||
PotionDetectMagic
|
||||
PotionRaiseLevel
|
||||
PotionExtraHealing
|
||||
PotionHaste
|
||||
PotionRestoreStrength
|
||||
PotionBlindness
|
||||
PotionLevitation
|
||||
NumPotionTypes
|
||||
)
|
||||
|
||||
// ScrollKind identifies a scroll (rogue.h scroll types).
|
||||
type ScrollKind int
|
||||
|
||||
// Scroll kinds (rogue.h)
|
||||
const (
|
||||
ScrollMonsterConfusion ScrollKind = iota
|
||||
ScrollMagicMapping
|
||||
ScrollHoldMonster
|
||||
ScrollSleep
|
||||
ScrollEnchantArmor
|
||||
ScrollIdentifyPotion
|
||||
ScrollIdentifyScroll
|
||||
ScrollIdentifyWeapon
|
||||
ScrollIdentifyArmor
|
||||
ScrollIdentifyRingOrStick
|
||||
ScrollScareMonster
|
||||
ScrollFoodDetection
|
||||
ScrollTeleportation
|
||||
ScrollEnchantWeapon
|
||||
ScrollCreateMonster
|
||||
ScrollRemoveCurse
|
||||
ScrollAggravateMonsters
|
||||
ScrollProtectArmor
|
||||
NumScrollTypes
|
||||
)
|
||||
|
||||
// WeaponKind identifies a weapon (rogue.h weapon types).
|
||||
type WeaponKind int
|
||||
|
||||
// Weapon kinds (rogue.h)
|
||||
const (
|
||||
WeaponMace WeaponKind = iota
|
||||
WeaponLongSword
|
||||
WeaponBow
|
||||
WeaponArrow
|
||||
WeaponDagger
|
||||
WeaponTwoHandedSword
|
||||
WeaponDart
|
||||
WeaponShuriken
|
||||
WeaponSpear
|
||||
WeaponFlame // fake entry for dragon breath (ick)
|
||||
)
|
||||
|
||||
// NumWeaponTypes counts the real weapons; the flame pseudo-weapon sits
|
||||
// just past them in the tables (C's MAXWEAPONS == FLAME).
|
||||
const NumWeaponTypes = WeaponFlame
|
||||
|
||||
// ArmorKind identifies a suit of armor (rogue.h armor types).
|
||||
type ArmorKind int
|
||||
|
||||
// Armor kinds (rogue.h)
|
||||
const (
|
||||
ArmorLeather ArmorKind = iota
|
||||
ArmorRingMail
|
||||
ArmorStuddedLeather
|
||||
ArmorScaleMail
|
||||
ArmorChainMail
|
||||
ArmorSplintMail
|
||||
ArmorBandedMail
|
||||
ArmorPlateMail
|
||||
NumArmorTypes
|
||||
)
|
||||
|
||||
// RingKind identifies a ring (rogue.h ring types).
|
||||
type RingKind int
|
||||
|
||||
// Ring kinds (rogue.h)
|
||||
const (
|
||||
RingProtection RingKind = iota
|
||||
RingAddStrength
|
||||
RingSustainStrength
|
||||
RingSearching
|
||||
RingSeeInvisible
|
||||
RingAdornment
|
||||
RingAggravateMonsters
|
||||
RingDexterity
|
||||
RingIncreaseDamage
|
||||
RingRegeneration
|
||||
RingSlowDigestion
|
||||
RingTeleportation
|
||||
RingStealth
|
||||
RingMaintainArmor
|
||||
NumRingTypes
|
||||
)
|
||||
|
||||
// WandKind identifies a wand or staff (rogue.h rod/wand/staff types).
|
||||
type WandKind int
|
||||
|
||||
// Wand kinds (rogue.h)
|
||||
const (
|
||||
WandLight WandKind = iota
|
||||
WandInvisibility
|
||||
WandLightning
|
||||
WandFire
|
||||
WandCold
|
||||
WandPolymorph
|
||||
WandMagicMissile
|
||||
WandHasteMonster
|
||||
WandSlowMonster
|
||||
WandDrainLife
|
||||
WandNothing
|
||||
WandTeleportAway
|
||||
WandTeleportTo
|
||||
WandCancellation
|
||||
NumWandTypes
|
||||
)
|
||||
|
||||
// Coord is a position on the level (rogue.h coord). A value type: the C
|
||||
// ce(a,b) macro is plain == here.
|
||||
type Coord struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// Stats describes a fighting being (rogue.h struct stats).
|
||||
type Stats struct {
|
||||
Str int // strength (s_str; 3..31)
|
||||
Exp int // experience
|
||||
Lvl int // level of mastery
|
||||
ArmorClass int // armor class (s_arm)
|
||||
HP int // hit points (s_hpt)
|
||||
Dmg DiceSpec // damage dice, e.g. "1x4/1x2"
|
||||
MaxHP int
|
||||
}
|
||||
|
||||
// Room describes a room or passage network (rogue.h struct room).
|
||||
type Room struct {
|
||||
Pos Coord // upper left corner
|
||||
Max Coord // size of room
|
||||
Gold Coord // where the gold is
|
||||
GoldVal int // how much the gold is worth
|
||||
Flags RoomFlags
|
||||
Exits []Coord // where the exits are (r_exit[12]/r_nexits)
|
||||
}
|
||||
|
||||
// MonsterKind is a bestiary entry (rogue.h struct monster).
|
||||
type MonsterKind struct {
|
||||
Name string // what to call the monster
|
||||
Carry int // probability of carrying something
|
||||
Flags CreatureFlags
|
||||
Stats Stats // initial stats
|
||||
}
|
||||
|
||||
// ObjInfo describes one object class: its real name, generation
|
||||
// probability, score worth, and per-game identification state
|
||||
// (rogue.h struct obj_info).
|
||||
type ObjInfo struct {
|
||||
Name string
|
||||
Prob int
|
||||
Worth int
|
||||
Guess string
|
||||
Know bool
|
||||
}
|
||||
|
||||
// Stone is a ring stone with its worth (rogue.h STONE).
|
||||
type Stone struct {
|
||||
Name string
|
||||
Value int
|
||||
}
|
||||
|
||||
// CTRL maps a letter to its control character, as the C CTRL() macro.
|
||||
func CTRL(c byte) byte { return c & 0o37 } //nolint:mnd // the C CTRL() mask
|
||||
|
||||
// distance returns the squared distance between two points (chase.c dist).
|
||||
func distance(y1, x1, y2, x2 int) int {
|
||||
dx := x2 - x1
|
||||
dy := y2 - y1
|
||||
|
||||
return dx*dx + dy*dy
|
||||
}
|
||||
|
||||
// distCp is dist_cp from chase.c.
|
||||
func distCp(c1, c2 Coord) int { return distance(c1.Y, c1.X, c2.Y, c2.X) }
|
||||
|
||||
// sign is misc.c sign(): -1, 0 or 1.
|
||||
func sign(nm int) int {
|
||||
switch {
|
||||
case nm < 0:
|
||||
return -1
|
||||
case nm > 0:
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
235
game/weapons.go
235
game/weapons.go
@@ -1,235 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import "fmt"
|
||||
|
||||
// weapons.c — functions for dealing with problems brought about by weapons.
|
||||
|
||||
const noWeapon WeaponKind = -1
|
||||
|
||||
// missile fires a missile in a given direction (weapons.c missile).
|
||||
func (g *RogueGame) missile(ydelta, xdelta int) {
|
||||
// Get which thing we are hurling
|
||||
obj, ok := g.promptPackItem("throw", KindWeapon)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if !g.dropCheck(obj) || g.isCurrent(obj) {
|
||||
return
|
||||
}
|
||||
|
||||
obj = g.leavePack(obj, true, false)
|
||||
g.doMotion(obj, ydelta, xdelta)
|
||||
// AHA! Here it has hit something. If it is a wall or a door, or if
|
||||
// it misses (combat) the monster, put it on the floor
|
||||
if g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil ||
|
||||
!g.hitMonster(obj.Pos, obj) {
|
||||
g.fall(obj, true)
|
||||
}
|
||||
}
|
||||
|
||||
// doMotion does the actual motion on the screen done by an object
|
||||
// traveling across the room (weapons.c do_motion).
|
||||
func (g *RogueGame) doMotion(obj *Object, ydelta, xdelta int) {
|
||||
p := &g.Player
|
||||
// Come fly with us ...
|
||||
obj.Pos = p.Pos
|
||||
for {
|
||||
g.eraseFlight(obj, p.Pos)
|
||||
// Get the new position
|
||||
obj.Pos.Y += ydelta
|
||||
obj.Pos.X += xdelta
|
||||
|
||||
ch := g.Level.VisibleChar(obj.Pos.Y, obj.Pos.X)
|
||||
if !stepOk(ch) || ch == Door {
|
||||
break
|
||||
}
|
||||
// It hasn't hit anything yet, so display it if it's alright.
|
||||
if g.canSee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
|
||||
g.mvaddch(obj.Pos.Y, obj.Pos.X, obj.Kind.Glyph())
|
||||
g.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eraseFlight erases a flying object from its current square, unless it
|
||||
// still sits on the hero (the erase step of weapons.c do_motion).
|
||||
func (g *RogueGame) eraseFlight(obj *Object, heroPos Coord) {
|
||||
if obj.Pos == heroPos || !g.canSee(obj.Pos.Y, obj.Pos.X) || g.Options.Terse {
|
||||
return
|
||||
}
|
||||
|
||||
ch := g.Level.Char(obj.Pos.Y, obj.Pos.X)
|
||||
if ch == Floor && !g.showFloor() {
|
||||
ch = ' '
|
||||
}
|
||||
|
||||
g.mvaddch(obj.Pos.Y, obj.Pos.X, ch)
|
||||
}
|
||||
|
||||
// fall drops an item someplace around here (weapons.c fall).
|
||||
func (g *RogueGame) fall(obj *Object, pr bool) {
|
||||
if fpos, ok := g.fallpos(obj.Pos); ok {
|
||||
pp := g.Level.At(fpos.Y, fpos.X)
|
||||
pp.Ch = obj.Kind.Glyph()
|
||||
|
||||
obj.Pos = fpos
|
||||
if g.canSee(fpos.Y, fpos.X) {
|
||||
if pp.Monst != nil {
|
||||
pp.Monst.OldCh = obj.Kind.Glyph()
|
||||
} else {
|
||||
g.mvaddch(fpos.Y, fpos.X, obj.Kind.Glyph())
|
||||
}
|
||||
}
|
||||
|
||||
g.Level.AddObject(obj)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if pr {
|
||||
if g.HasHit {
|
||||
g.endmsg()
|
||||
g.HasHit = false
|
||||
}
|
||||
|
||||
g.msg("the %s vanishes as it hits the ground",
|
||||
g.Items.Weapons[obj.Which].Name)
|
||||
}
|
||||
}
|
||||
|
||||
// hitMonster checks if the missile hits the monster (weapons.c
|
||||
// hit_monster).
|
||||
func (g *RogueGame) hitMonster(mp Coord, obj *Object) bool {
|
||||
return g.fight(mp, obj, true)
|
||||
}
|
||||
|
||||
// wield pulls out a certain weapon (weapons.c wield).
|
||||
func (g *RogueGame) wield() {
|
||||
p := &g.Player
|
||||
|
||||
oweapon := p.CurWeapon
|
||||
if !g.dropCheck(p.CurWeapon) {
|
||||
p.CurWeapon = oweapon
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.CurWeapon = oweapon
|
||||
|
||||
obj, ok := g.promptPackItem("wield", KindWeapon)
|
||||
if !ok {
|
||||
g.After = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if obj.Kind == KindArmor {
|
||||
g.msg("you can't wield armor")
|
||||
g.After = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if g.isCurrent(obj) {
|
||||
g.After = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sp := g.inventoryName(obj, true)
|
||||
p.CurWeapon = obj
|
||||
|
||||
if !g.Options.Terse {
|
||||
g.addmsgf("you are now ")
|
||||
}
|
||||
|
||||
g.msg("wielding %s (%c)", sp, obj.PackCh)
|
||||
}
|
||||
|
||||
// weaponSetup is one row of the weapons.c init_dam[] table (see
|
||||
// gameData.initWeaps).
|
||||
type weaponSetup struct {
|
||||
dam DiceSpec // damage when wielded
|
||||
hrl DiceSpec // damage when thrown
|
||||
launch WeaponKind // launching weapon
|
||||
flags ObjFlags
|
||||
}
|
||||
|
||||
// initWeapon sets up a new weapon (weapons.c init_weapon).
|
||||
func (g *RogueGame) initWeapon(weap *Object, which WeaponKind) {
|
||||
// init_dam[] has a row only for the real weapons: WeaponFlame (dragon
|
||||
// breath) and anything past it have none. createObj rejects such a
|
||||
// choice before calling here, so this arm is unreachable in practice;
|
||||
// it exists so a malformed kind leaves the weapon untouched instead of
|
||||
// panicking on the table read.
|
||||
if which < 0 || int(which) >= int(NumWeaponTypes) {
|
||||
return
|
||||
}
|
||||
|
||||
iwp := &g.data.initWeaps[which]
|
||||
weap.Kind = KindWeapon
|
||||
weap.Which = int(which)
|
||||
weap.Damage = iwp.dam
|
||||
weap.HurlDmg = iwp.hrl
|
||||
weap.Launch = iwp.launch
|
||||
weap.Flags = iwp.flags
|
||||
weap.HPlus = 0
|
||||
|
||||
weap.DPlus = 0
|
||||
|
||||
switch {
|
||||
case which == WeaponDagger:
|
||||
weap.Count = g.rnd(4) + 2
|
||||
weap.Group = g.Items.Group
|
||||
g.Items.Group++
|
||||
case weap.Flags.Has(Stackable):
|
||||
weap.Count = g.rnd(8) + 8
|
||||
weap.Group = g.Items.Group
|
||||
g.Items.Group++
|
||||
default:
|
||||
weap.Count = 1
|
||||
weap.Group = 0
|
||||
}
|
||||
}
|
||||
|
||||
// num formats a hit/damage or armor bonus string (weapons.c num).
|
||||
func num(n1, n2 int, typ byte) string {
|
||||
out := fmt.Sprintf("%+d", n1)
|
||||
if typ == Weapon {
|
||||
out += fmt.Sprintf(",%+d", n2)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// fallpos picks a random empty position around (pos) for a dropped item
|
||||
// (weapons.c fallpos).
|
||||
func (g *RogueGame) fallpos(pos Coord) (Coord, bool) {
|
||||
var newpos Coord
|
||||
|
||||
cnt := 0
|
||||
|
||||
for y := pos.Y - 1; y <= pos.Y+1; y++ {
|
||||
for x := pos.X - 1; x <= pos.X+1; x++ {
|
||||
// check to make certain the spot is empty, if it is, put the
|
||||
// object there, set it in the level list and re-draw the room
|
||||
// if he can see it
|
||||
if (y == g.Player.Pos.Y && x == g.Player.Pos.X) ||
|
||||
y < 0 || x < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
ch := g.Level.Char(y, x)
|
||||
if ch == Floor || ch == Passage {
|
||||
if cnt++; g.rnd(cnt) == 0 {
|
||||
newpos.Y = y
|
||||
newpos.X = x
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newpos, cnt != 0
|
||||
}
|
||||
247
game/wizard.go
247
game/wizard.go
@@ -1,247 +0,0 @@
|
||||
//nolint:mnd // C-faithful literals; names hurt C-greppability (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
// wizard.c — special wizard commands, some of which are also non-wizard
|
||||
// commands under strange circumstances. The DES password check is not
|
||||
// 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.Kind = objectKindForGlyph(g.readchar())
|
||||
g.Msgs.Mpos = 0
|
||||
g.msg("which %c do you want? (0-f)", obj.Kind.Glyph())
|
||||
|
||||
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
|
||||
|
||||
// Deliberate divergence from 5.4.4: C stored this nibble unchecked, so
|
||||
// 'a'-'f' indexed straight past the ends of the per-kind static
|
||||
// tables. Input outside '0'-'9' and 'a'-'f' overshoots much further:
|
||||
// readchar returns a byte, so ch-'a' above is byte arithmetic and
|
||||
// wraps instead of going negative ('A' gives 234, '!' gives 202).
|
||||
// Reading past a static array was undefined behavior C happened to
|
||||
// survive by picking up adjacent memory; in Go it is a panic that
|
||||
// kills the process with the terminal still in raw mode. C had no
|
||||
// defined behavior here to be faithful to, so the choice is rejected
|
||||
// outright rather than emulating a garbage read. The check precedes
|
||||
// every rnd() call below, so the RNG sequence is untouched either way.
|
||||
if !obj.wizardCanCreate() {
|
||||
g.msg("there is no such %s", obj.Kind)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.Kind {
|
||||
case KindWeapon, KindArmor:
|
||||
g.createWeaponArmor(obj)
|
||||
case KindRing:
|
||||
g.createRing(obj)
|
||||
case KindWand:
|
||||
g.fixStick(obj)
|
||||
case KindGold:
|
||||
g.msg("how much?")
|
||||
|
||||
buf := ""
|
||||
if g.getStr(&buf, g.scr.Std) == Norm {
|
||||
obj.GoldValue = cAtoi(buf)
|
||||
}
|
||||
}
|
||||
|
||||
g.addPack(obj, false)
|
||||
}
|
||||
|
||||
// createWeaponArmor sets up a wizard-created weapon or armor with an
|
||||
// optional blessing (the weapon/armor arm of wizard.c create_obj).
|
||||
func (g *RogueGame) createWeaponArmor(obj *Object) {
|
||||
g.msg("blessing? (+,-,n)")
|
||||
bless := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if bless == '-' {
|
||||
obj.Flags.Set(Cursed)
|
||||
}
|
||||
|
||||
if obj.Kind == KindWeapon {
|
||||
g.initWeapon(obj, WeaponKind(obj.Which))
|
||||
|
||||
if bless == '-' {
|
||||
obj.HPlus -= g.rnd(3) + 1
|
||||
}
|
||||
|
||||
if bless == '+' {
|
||||
obj.HPlus += g.rnd(3) + 1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
obj.ArmorClass = g.data.armorClass(obj.Which)
|
||||
if bless == '-' {
|
||||
obj.ArmorClass += g.rnd(3) + 1
|
||||
}
|
||||
|
||||
if bless == '+' {
|
||||
obj.ArmorClass -= g.rnd(3) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// createRing sets up a wizard-created ring, prompting for a bonus on
|
||||
// the bonus rings (the ring arm of wizard.c create_obj).
|
||||
func (g *RogueGame) createRing(obj *Object) {
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.RingKind() {
|
||||
case RingProtection, RingAddStrength, RingDexterity, RingIncreaseDamage:
|
||||
g.msg("blessing? (+,-,n)")
|
||||
bless := g.readchar()
|
||||
g.Msgs.Mpos = 0
|
||||
|
||||
if bless == '-' {
|
||||
obj.Flags.Set(Cursed)
|
||||
obj.Bonus = -1
|
||||
} else {
|
||||
obj.Bonus = g.rnd(2) + 1
|
||||
}
|
||||
case RingAggravateMonsters, RingTeleportation:
|
||||
obj.Flags.Set(Cursed)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := range NumCols {
|
||||
isReal := g.Level.FlagsAt(y, x).Has(FReal)
|
||||
if !isReal {
|
||||
hw.Standout(true)
|
||||
}
|
||||
|
||||
hw.MvAddCh(y, x, g.Level.Char(y, x))
|
||||
|
||||
if !isReal {
|
||||
hw.Standout(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.showWin("---More (level map)---")
|
||||
}
|
||||
|
||||
// whatis identifies what a certain object is (wizard.c whatis).
|
||||
func (g *RogueGame) whatis(insist bool, kind ObjectKind) {
|
||||
p := &g.Player
|
||||
if len(p.Pack) == 0 {
|
||||
g.msg("you don't have anything in your pack to identify")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
obj, ok := g.whatisPick(insist, kind)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:exhaustive // C-faithful: only the cases C handled (approved 2026-07-07)
|
||||
switch obj.Kind {
|
||||
case KindScroll:
|
||||
setKnow(obj, g.Items.Scrolls[:])
|
||||
case KindPotion:
|
||||
setKnow(obj, g.Items.Potions[:])
|
||||
case KindWand:
|
||||
setKnow(obj, g.Items.Sticks[:])
|
||||
case KindWeapon, KindArmor:
|
||||
obj.Flags.Set(Known)
|
||||
case KindRing:
|
||||
setKnow(obj, g.Items.Rings[:])
|
||||
}
|
||||
|
||||
g.msg("%s", g.inventoryName(obj, false))
|
||||
}
|
||||
|
||||
// whatisPick prompts for the item to identify, re-asking until a
|
||||
// matching one is chosen when insist is set; ok is false when the
|
||||
// player gives up (the prompt loop of wizard.c whatis).
|
||||
func (g *RogueGame) whatisPick(insist bool, kind ObjectKind) (*Object, bool) {
|
||||
for {
|
||||
obj, _ := g.promptPackItem("identify", kind)
|
||||
|
||||
if !insist {
|
||||
return obj, obj != nil
|
||||
}
|
||||
|
||||
if g.NObjs == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if obj == nil {
|
||||
g.msg("you must identify something")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !matchesFilter(kind, obj) {
|
||||
g.msg("you must identify a %s", kind)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return obj, true
|
||||
}
|
||||
}
|
||||
|
||||
// setKnow sets things up when we really know what a thing is (wizard.c
|
||||
// set_know).
|
||||
func setKnow(obj *Object, info []ObjInfo) {
|
||||
info[obj.Which].Know = true
|
||||
obj.Flags.Set(Known)
|
||||
info[obj.Which].Guess = ""
|
||||
}
|
||||
|
||||
// The C type_name()/tlist table is gone: ObjectKind.String() carries the
|
||||
// same vocabulary.
|
||||
|
||||
// teleport bamfs the hero someplace else (wizard.c teleport).
|
||||
func (g *RogueGame) teleport() {
|
||||
p := &g.Player
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
|
||||
|
||||
c, _ := g.findFloor(true)
|
||||
if g.roomIn(c) != p.Room {
|
||||
g.leaveRoom(p.Pos)
|
||||
p.Pos = c
|
||||
g.enterRoom(p.Pos)
|
||||
} else {
|
||||
p.Pos = c
|
||||
|
||||
g.look(true)
|
||||
}
|
||||
|
||||
g.mvaddch(p.Pos.Y, p.Pos.X, PlayerCh)
|
||||
// turn off ISHELD in case teleportation was done while fighting a
|
||||
// Flytrap
|
||||
if p.On(Held) {
|
||||
p.Flags.Clear(Held)
|
||||
p.VfHit = 0
|
||||
g.Monsters['F'-'A'].Stats.Dmg = dice("000x0")
|
||||
}
|
||||
|
||||
g.NoMove = 0
|
||||
g.Count = 0
|
||||
g.Running = false
|
||||
g.flushType()
|
||||
}
|
||||
@@ -1,532 +0,0 @@
|
||||
//nolint:testpackage // white-box tests reach unexported state (approved 2026-07-07)
|
||||
package game
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mustNotPanic runs fn and turns a panic into an ordinary test failure.
|
||||
// The bug these tests cover (issue #10) panicked out of an array index,
|
||||
// and an unrecovered panic would take the whole test binary down instead
|
||||
// of reporting which dispatch regressed.
|
||||
func mustNotPanic(t *testing.T, what string, fn func()) {
|
||||
t.Helper()
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("%s panicked: %v", what, r)
|
||||
}
|
||||
}()
|
||||
|
||||
fn()
|
||||
}
|
||||
|
||||
// TestCreateObjWandFReproducer is the exact reported crash: wizard mode,
|
||||
// C, '/' for a wand, 'f' for which. 'f' is nibble 15 and there are only
|
||||
// NumWandTypes (14) wands, so fixStick used to index two past the end of
|
||||
// ws_type[] and panic.
|
||||
func TestCreateObjWandFReproducer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
g.Wizard = true
|
||||
before := len(g.Player.Pack)
|
||||
|
||||
setInput(t, g, '/', 'f')
|
||||
|
||||
mustNotPanic(t, "createObj with wand 'f'", g.createObj)
|
||||
|
||||
if len(g.Player.Pack) != before {
|
||||
t.Errorf("out-of-range wand was added to the pack: %d items, want %d",
|
||||
len(g.Player.Pack), before)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateObjRejectsOutOfRangeWhich sweeps the rejection across every
|
||||
// kind whose Which is a table index, including input outside '0'-'f'.
|
||||
// isDigit is false for such input, so it takes the letter branch, where
|
||||
// ch-'a' is byte arithmetic and wraps rather than going negative: 'A'
|
||||
// (65) gives int(224)+10 == 234 and '!' (33) gives int(192)+10 == 202.
|
||||
// Those far-past-the-end values, not negative ones, are what the guard
|
||||
// has to catch on the keyboard path.
|
||||
func TestCreateObjRejectsOutOfRangeWhich(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
typ byte
|
||||
which byte
|
||||
}{
|
||||
{"wand f is past NumWandTypes", Stick, 'f'},
|
||||
{"potion f is past NumPotionTypes", Potion, 'f'},
|
||||
{"ring f is past NumRingTypes", Ring, 'f'},
|
||||
// NumScrollTypes is 18, past the 'f' the prompt tops out at, so a
|
||||
// scroll can only be driven out of range by input that wraps.
|
||||
{"scroll 'A' wraps to 234", Scroll, 'A'},
|
||||
{"armor 9 is past NumArmorTypes", Armor, '9'},
|
||||
{"weapon 9 is the flame pseudo-weapon", Weapon, '9'},
|
||||
{"wand 'A' wraps to 234", Stick, 'A'},
|
||||
{"wand '!' wraps to 202", Stick, '!'},
|
||||
{"armor 'A' wraps to 234", Armor, 'A'},
|
||||
{"weapon '!' wraps to 202", Weapon, '!'},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
g.Wizard = true
|
||||
before := len(g.Player.Pack)
|
||||
|
||||
setInput(t, g, tc.typ, tc.which)
|
||||
|
||||
mustNotPanic(t, tc.name, g.createObj)
|
||||
|
||||
if len(g.Player.Pack) != before {
|
||||
t.Errorf("pack grew to %d items, want %d: a rejected item was created",
|
||||
len(g.Player.Pack), before)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateObjAcceptsValidWhich pins the other half of the contract: the
|
||||
// bounds check must not touch any in-range choice.
|
||||
func TestCreateObjAcceptsValidWhich(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
typ byte
|
||||
which byte
|
||||
kind ObjectKind
|
||||
want int
|
||||
}{
|
||||
{"wand of light", Stick, '0', KindWand, int(WandLight)},
|
||||
{"potion 0", Potion, '0', KindPotion, int(PotionConfusion)},
|
||||
{"scroll 9", Scroll, '9', KindScroll, int(ScrollIdentifyRingOrStick)},
|
||||
{"ring d, the last ring", Ring, 'd', KindRing, int(NumRingTypes) - 1},
|
||||
{"armor 7, the last armor", Armor, '7', KindArmor, int(NumArmorTypes) - 1},
|
||||
{"weapon 8, the last real weapon", Weapon, '8', KindWeapon, int(WeaponSpear)},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
g.Wizard = true
|
||||
|
||||
before := make(map[*Object]bool, len(g.Player.Pack))
|
||||
for _, o := range g.Player.Pack {
|
||||
before[o] = true
|
||||
}
|
||||
|
||||
// The armor and weapon arms read one more character for the
|
||||
// blessing prompt; 'n' means neither cursed nor blessed.
|
||||
setInput(t, g, tc.typ, tc.which, 'n')
|
||||
g.createObj()
|
||||
|
||||
if len(g.Player.Pack) != len(before)+1 {
|
||||
t.Fatalf("pack has %d items, want %d: valid item not created",
|
||||
len(g.Player.Pack), len(before)+1)
|
||||
}
|
||||
|
||||
// addPack files the new item in kind order, so find it by
|
||||
// identity rather than assuming it landed at the end.
|
||||
var made *Object
|
||||
|
||||
for _, o := range g.Player.Pack {
|
||||
if !before[o] {
|
||||
made = o
|
||||
}
|
||||
}
|
||||
|
||||
if made.Kind != tc.kind || made.Which != tc.want {
|
||||
t.Errorf("created %v which %d, want %v which %d",
|
||||
made.Kind, made.Which, tc.kind, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateObjKeepsRNGSequence proves the guard costs no RNG draws: a
|
||||
// rejected creation must leave the generator exactly where it was, or
|
||||
// every later roll in the game would shift.
|
||||
func TestCreateObjKeepsRNGSequence(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
g.Wizard = true
|
||||
before := g.Rng.Seed
|
||||
|
||||
setInput(t, g, Stick, 'f')
|
||||
g.createObj()
|
||||
|
||||
if g.Rng.Seed != before {
|
||||
t.Errorf("rejected creation consumed RNG: seed %d, want %d",
|
||||
g.Rng.Seed, before)
|
||||
}
|
||||
}
|
||||
|
||||
// malformed builds an object of the given kind whose Which sits one past
|
||||
// the end of that kind's table — the state the wizard bug used to leave
|
||||
// behind, and the state a corrupt save file could still describe.
|
||||
func malformed(kind ObjectKind) *Object {
|
||||
obj := newObject()
|
||||
obj.Kind = kind
|
||||
obj.Which = whichLimit(kind)
|
||||
obj.Count = 1
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// TestZapMalformedWandDoesNotPanic covers the sticks.go dispatch. C's
|
||||
// do_zap matched no case and still ran o_charges--, so the charge must be
|
||||
// spent even though the zap did nothing. What it says while doing nothing
|
||||
// belongs to TestZapUnhandledWandSaysBizarreSchtick.
|
||||
func TestZapMalformedWandDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
wand := malformed(KindWand)
|
||||
wand.Charges = 3
|
||||
ch := give(g, wand)
|
||||
|
||||
setInput(t, g, ch)
|
||||
|
||||
mustNotPanic(t, "doZap on a malformed wand", g.doZap)
|
||||
|
||||
if wand.Charges != 2 {
|
||||
t.Errorf("charges = %d after zapping, want 2", wand.Charges)
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuaffMalformedPotionDoesNotPanic covers the potions.go dispatch and
|
||||
// the callIt lookup that follows it.
|
||||
func TestQuaffMalformedPotionDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
before := len(g.Player.Pack)
|
||||
ch := give(g, malformed(KindPotion))
|
||||
|
||||
setInput(t, g, ch)
|
||||
|
||||
mustNotPanic(t, "quaff of a malformed potion", g.quaff)
|
||||
|
||||
if len(g.Player.Pack) != before {
|
||||
t.Errorf("pack has %d items, want %d: the potion was not consumed",
|
||||
len(g.Player.Pack), before)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadMalformedScrollDoesNotPanic covers the scrolls.go dispatch and
|
||||
// its callIt lookup.
|
||||
func TestReadMalformedScrollDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
before := len(g.Player.Pack)
|
||||
ch := give(g, malformed(KindScroll))
|
||||
|
||||
setInput(t, g, ch)
|
||||
|
||||
mustNotPanic(t, "readScroll of a malformed scroll", g.readScroll)
|
||||
|
||||
if len(g.Player.Pack) != before {
|
||||
t.Errorf("pack has %d items, want %d: the scroll was not consumed",
|
||||
len(g.Player.Pack), before)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMalformedArmorDoesNotPanic covers the a_class[] reads: pricing at
|
||||
// death, the identified-armor name, and the detect-magic test.
|
||||
func TestMalformedArmorDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
armor := malformed(KindArmor)
|
||||
armor.Flags.Set(Known)
|
||||
|
||||
mustNotPanic(t, "naming a malformed suit of armor", func() {
|
||||
if got := g.inventoryName(armor, false); got != armor.Kind.String() {
|
||||
t.Errorf("inventoryName = %q, want %q", got, armor.Kind.String())
|
||||
}
|
||||
})
|
||||
|
||||
mustNotPanic(t, "isMagic on a malformed suit of armor", func() {
|
||||
g.isMagic(armor)
|
||||
})
|
||||
|
||||
mustNotPanic(t, "appraising a malformed suit of armor", func() {
|
||||
if worth := g.objectWorth(armor); worth != 0 {
|
||||
t.Errorf("objectWorth = %d, want 0", worth)
|
||||
}
|
||||
})
|
||||
|
||||
if got := g.data.armorClass(armor.Which); got != 0 {
|
||||
t.Errorf("armorClass(%d) = %d, want 0", armor.Which, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMalformedWeaponDoesNotPanic covers the init_dam[] read. WeaponFlame
|
||||
// is the first kind with no table row, so initWeapon must leave the
|
||||
// object alone rather than index past the end.
|
||||
func TestMalformedWeaponDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
weap := newObject()
|
||||
|
||||
mustNotPanic(t, "initWeapon with the flame pseudo-weapon", func() {
|
||||
g.initWeapon(weap, WeaponFlame)
|
||||
})
|
||||
|
||||
mustNotPanic(t, "initWeapon with a negative weapon kind", func() {
|
||||
g.initWeapon(weap, WeaponKind(-1))
|
||||
})
|
||||
|
||||
if weap.Kind != KindNone {
|
||||
t.Errorf("weapon was initialized from a missing table row: kind %v",
|
||||
weap.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFixStickMalformedWhichDoesNotPanic covers the ws_type[] read that
|
||||
// the reported reproducer actually crashed on.
|
||||
func TestFixStickMalformedWhichDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGameInput(t)
|
||||
wand := malformed(KindWand)
|
||||
|
||||
mustNotPanic(t, "fixStick on a malformed wand", func() {
|
||||
g.fixStick(wand)
|
||||
})
|
||||
|
||||
if wand.Damage.String() != "1x1" {
|
||||
t.Errorf("damage = %q, want the wand damage %q",
|
||||
wand.Damage.String(), "1x1")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRestoreRejectsOutOfRangeWhich is the save-file half of the fix: a
|
||||
// malformed object must not be able to sneak past the keyboard guard by
|
||||
// arriving in a snapshot.
|
||||
//
|
||||
// A decoded save is also the only place a *negative* Which can come
|
||||
// from. On the keyboard path createObj's ch-'a' is byte arithmetic and
|
||||
// wraps, so 'A' and '!' land at 234 and 202; Which is a plain int in the
|
||||
// gob stream, so a tampered file can carry any value at all. Both shapes
|
||||
// are covered here, and the negative case is what exercises the
|
||||
// Which >= 0 arm of hasValidWhich.
|
||||
func TestRestoreRejectsOutOfRangeWhich(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
which int
|
||||
}{
|
||||
{"one past the wand table", int(NumWandTypes)},
|
||||
{"the value 'A' wraps to on the keyboard path", 234},
|
||||
{"the value '!' wraps to on the keyboard path", 202},
|
||||
{"negative, reachable only from a tampered file", -1},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 11)
|
||||
st := g.snapshot()
|
||||
|
||||
if len(st.Player.Body.Pack) == 0 {
|
||||
t.Fatal("starting pack is empty; nothing to corrupt")
|
||||
}
|
||||
|
||||
st.Player.Body.Pack[0].Kind = KindWand
|
||||
st.Player.Body.Pack[0].Which = tc.which
|
||||
|
||||
path := filepath.Join(t.TempDir(), "rogue.save")
|
||||
writeSnapshot(t, path, st)
|
||||
|
||||
_, restoreErr := Restore(path, Params{Term: &testTerm{}})
|
||||
if !errors.Is(restoreErr, ErrSaveCorrupt) {
|
||||
t.Errorf("Restore error = %v, want ErrSaveCorrupt", restoreErr)
|
||||
}
|
||||
|
||||
_, statErr := os.Stat(path)
|
||||
if statErr != nil {
|
||||
t.Error("a rejected save file was deleted; it should be left alone")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// writeSnapshot gob-encodes a snapshot to path the way saveFile does.
|
||||
func writeSnapshot(t *testing.T, path string, st *SaveState) {
|
||||
t.Helper()
|
||||
|
||||
f, err := os.Create(path) //nolint:gosec // G304: test temp path
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
encErr := gob.NewEncoder(f).Encode(st)
|
||||
if encErr != nil {
|
||||
t.Fatal(encErr)
|
||||
}
|
||||
|
||||
closeErr := f.Close()
|
||||
if closeErr != nil {
|
||||
t.Fatal(closeErr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWhichLimitCoversEveryIndexedTable pins the bounds table itself
|
||||
// against the per-kind arrays it has to agree with.
|
||||
func TestWhichLimitCoversEveryIndexedTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// len() of an array field is a compile-time constant, so the zero
|
||||
// value is enough to read the table sizes off.
|
||||
var it ItemLore
|
||||
|
||||
cases := []struct {
|
||||
kind ObjectKind
|
||||
size int
|
||||
}{
|
||||
{KindPotion, len(it.Potions)},
|
||||
{KindScroll, len(it.Scrolls)},
|
||||
{KindRing, len(it.Rings)},
|
||||
{KindWand, len(it.Sticks)},
|
||||
{KindArmor, len(it.Armors)},
|
||||
{KindWeapon, len(it.Weapons)},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
if got := whichLimit(tc.kind); got != tc.size {
|
||||
t.Errorf("whichLimit(%v) = %d, want the table size %d",
|
||||
tc.kind, got, tc.size)
|
||||
}
|
||||
}
|
||||
|
||||
// Kinds whose Which is not a table index accept anything, as in C.
|
||||
for _, kind := range []ObjectKind{KindFood, KindAmulet, KindGold, KindNone} {
|
||||
obj := &Object{Kind: kind, Which: 99}
|
||||
if !obj.hasValidWhich() {
|
||||
t.Errorf("%v should not be bounds-checked on Which", kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWizardToggleOffRehidesSensedMonsters drives '+' through command
|
||||
// dispatch in wizard mode (command.c 317-338). Clearing the flag is the
|
||||
// cheap half; the substantive half is turn_see(TRUE) — leaving wizard
|
||||
// mode has to put the screen back, or there is no way out of wizard
|
||||
// sight once it is on.
|
||||
func TestWizardToggleOffRehidesSensedMonsters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 11)
|
||||
g.Wizard = true
|
||||
|
||||
// A phantom carries ISINVIS straight from the monster table, so
|
||||
// seeMonst is false for it and it is on screen only because wizard
|
||||
// sight put it there.
|
||||
tp := spawnAdjacent(g, 'P')
|
||||
if !tp.On(Invisible) {
|
||||
t.Fatal("phantom is not invisible; this test needs an unseeable monster")
|
||||
}
|
||||
|
||||
if g.seeMonst(tp) {
|
||||
t.Fatal("monster is ordinarily visible; wizard sight would reveal nothing")
|
||||
}
|
||||
|
||||
if tp.OldCh == tp.Type {
|
||||
t.Fatalf("map char under the monster is also %q; the redraw "+
|
||||
"assertion would prove nothing", tp.Type)
|
||||
}
|
||||
|
||||
g.turnSee(false)
|
||||
|
||||
if !g.Player.On(SenseMonsters) {
|
||||
t.Fatal("turnSee(false) did not set SenseMonsters")
|
||||
}
|
||||
|
||||
if ch := g.mvinch(tp.Pos.Y, tp.Pos.X); ch != tp.Type {
|
||||
t.Fatalf("wizard sight did not draw the monster: cell is %q, want %q",
|
||||
ch, tp.Type)
|
||||
}
|
||||
|
||||
if !g.scr.Std.at(tp.Pos.Y, tp.Pos.X).standout {
|
||||
t.Fatal("wizard-sighted monster was not drawn in standout")
|
||||
}
|
||||
|
||||
g.Msgs.Mpos = 0
|
||||
g.After = true
|
||||
|
||||
g.dispatch('+')
|
||||
|
||||
if g.Wizard {
|
||||
t.Error("'+' did not clear the wizard flag")
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
t.Error("'+' left SenseMonsters set: turn_see(TRUE) was not performed")
|
||||
}
|
||||
|
||||
if ch := g.mvinch(tp.Pos.Y, tp.Pos.X); ch != tp.OldCh {
|
||||
t.Errorf("monster still on screen after leaving wizard mode: cell is "+
|
||||
"%q, want the map char under it, %q", ch, tp.OldCh)
|
||||
}
|
||||
|
||||
if g.scr.Std.at(tp.Pos.Y, tp.Pos.X).standout {
|
||||
t.Error("cell left in standout after leaving wizard mode")
|
||||
}
|
||||
|
||||
if g.Msgs.Huh != "not wizard any more" {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, "not wizard any more")
|
||||
}
|
||||
|
||||
if g.After {
|
||||
t.Error("'+' consumed a turn; C sets after = FALSE")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWizardToggleWithoutWizardSaysSorry pins the other arm. C ran
|
||||
// wizard = passwd() and said "sorry" when the answer was wrong; the
|
||||
// password machinery is dropped, so that is the only outcome left. What
|
||||
// it must not be any more is "illegal command '+'".
|
||||
func TestWizardToggleWithoutWizardSaysSorry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
g := mkGame(t, 12)
|
||||
g.Wizard = false
|
||||
g.Msgs.Mpos = 0
|
||||
g.After = true
|
||||
|
||||
g.dispatch('+')
|
||||
|
||||
if g.Wizard {
|
||||
t.Error("'+' entered wizard mode with no password check to pass")
|
||||
}
|
||||
|
||||
if g.Player.On(SenseMonsters) {
|
||||
t.Error("'+' turned on monster sense outside wizard mode")
|
||||
}
|
||||
|
||||
if g.Msgs.Huh != "sorry" {
|
||||
t.Errorf("message = %q, want %q", g.Msgs.Huh, "sorry")
|
||||
}
|
||||
|
||||
if g.After {
|
||||
t.Error("'+' consumed a turn; C sets after = FALSE")
|
||||
}
|
||||
}
|
||||
14
go.mod
14
go.mod
@@ -1,14 +0,0 @@
|
||||
module git.eeqj.de/sneak/rgoue
|
||||
|
||||
go 1.25.7
|
||||
|
||||
require github.com/gdamore/tcell/v2 v2.13.10
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.1 // 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
45
go.sum
@@ -1,45 +0,0 @@
|
||||
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=
|
||||
447
init.c
Normal file
447
init.c
Normal file
@@ -0,0 +1,447 @@
|
||||
/*
|
||||
* global variable initializaton
|
||||
*
|
||||
* @(#)init.c 4.31 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <curses.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* init_player:
|
||||
* Roll her up
|
||||
*/
|
||||
void
|
||||
init_player()
|
||||
{
|
||||
register THING *obj;
|
||||
|
||||
pstats = max_stats;
|
||||
food_left = HUNGERTIME;
|
||||
/*
|
||||
* Give him some food
|
||||
*/
|
||||
obj = new_item();
|
||||
obj->o_type = FOOD;
|
||||
obj->o_count = 1;
|
||||
add_pack(obj, TRUE);
|
||||
/*
|
||||
* And his suit of armor
|
||||
*/
|
||||
obj = new_item();
|
||||
obj->o_type = ARMOR;
|
||||
obj->o_which = RING_MAIL;
|
||||
obj->o_arm = a_class[RING_MAIL] - 1;
|
||||
obj->o_flags |= ISKNOW;
|
||||
obj->o_count = 1;
|
||||
cur_armor = obj;
|
||||
add_pack(obj, TRUE);
|
||||
/*
|
||||
* Give him his weaponry. First a mace.
|
||||
*/
|
||||
obj = new_item();
|
||||
init_weapon(obj, MACE);
|
||||
obj->o_hplus = 1;
|
||||
obj->o_dplus = 1;
|
||||
obj->o_flags |= ISKNOW;
|
||||
add_pack(obj, TRUE);
|
||||
cur_weapon = obj;
|
||||
/*
|
||||
* Now a +1 bow
|
||||
*/
|
||||
obj = new_item();
|
||||
init_weapon(obj, BOW);
|
||||
obj->o_hplus = 1;
|
||||
obj->o_flags |= ISKNOW;
|
||||
add_pack(obj, TRUE);
|
||||
/*
|
||||
* Now some arrows
|
||||
*/
|
||||
obj = new_item();
|
||||
init_weapon(obj, ARROW);
|
||||
obj->o_count = rnd(15) + 25;
|
||||
obj->o_flags |= ISKNOW;
|
||||
add_pack(obj, TRUE);
|
||||
}
|
||||
|
||||
/*
|
||||
* Contains defintions and functions for dealing with things like
|
||||
* potions and scrolls
|
||||
*/
|
||||
|
||||
char *rainbow[] = {
|
||||
"amber",
|
||||
"aquamarine",
|
||||
"black",
|
||||
"blue",
|
||||
"brown",
|
||||
"clear",
|
||||
"crimson",
|
||||
"cyan",
|
||||
"ecru",
|
||||
"gold",
|
||||
"green",
|
||||
"grey",
|
||||
"magenta",
|
||||
"orange",
|
||||
"pink",
|
||||
"plaid",
|
||||
"purple",
|
||||
"red",
|
||||
"silver",
|
||||
"tan",
|
||||
"tangerine",
|
||||
"topaz",
|
||||
"turquoise",
|
||||
"vermilion",
|
||||
"violet",
|
||||
"white",
|
||||
"yellow",
|
||||
};
|
||||
|
||||
#define NCOLORS (sizeof rainbow / sizeof (char *))
|
||||
int cNCOLORS = NCOLORS;
|
||||
|
||||
static char *sylls[] = {
|
||||
"a", "ab", "ag", "aks", "ala", "an", "app", "arg", "arze", "ash",
|
||||
"bek", "bie", "bit", "bjor", "blu", "bot", "bu", "byt", "comp",
|
||||
"con", "cos", "cre", "dalf", "dan", "den", "do", "e", "eep", "el",
|
||||
"eng", "er", "ere", "erk", "esh", "evs", "fa", "fid", "fri", "fu",
|
||||
"gan", "gar", "glen", "gop", "gre", "ha", "hyd", "i", "ing", "ip",
|
||||
"ish", "it", "ite", "iv", "jo", "kho", "kli", "klis", "la", "lech",
|
||||
"mar", "me", "mi", "mic", "mik", "mon", "mung", "mur", "nej",
|
||||
"nelg", "nep", "ner", "nes", "nes", "nih", "nin", "o", "od", "ood",
|
||||
"org", "orn", "ox", "oxy", "pay", "ple", "plu", "po", "pot",
|
||||
"prok", "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa",
|
||||
"san", "sat", "sef", "seh", "shu", "ski", "sna", "sne", "snik",
|
||||
"sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", "tem",
|
||||
"ther", "ti", "tox", "trol", "tue", "turs", "u", "ulk", "um", "un",
|
||||
"uni", "ur", "val", "viv", "vly", "vom", "wah", "wed", "werg",
|
||||
"wex", "whon", "wun", "xo", "y", "yot", "yu", "zant", "zeb", "zim",
|
||||
"zok", "zon", "zum",
|
||||
};
|
||||
|
||||
STONE stones[] = {
|
||||
{ "agate", 25},
|
||||
{ "alexandrite", 40},
|
||||
{ "amethyst", 50},
|
||||
{ "carnelian", 40},
|
||||
{ "diamond", 300},
|
||||
{ "emerald", 300},
|
||||
{ "germanium", 225},
|
||||
{ "granite", 5},
|
||||
{ "garnet", 50},
|
||||
{ "jade", 150},
|
||||
{ "kryptonite", 300},
|
||||
{ "lapis lazuli", 50},
|
||||
{ "moonstone", 50},
|
||||
{ "obsidian", 15},
|
||||
{ "onyx", 60},
|
||||
{ "opal", 200},
|
||||
{ "pearl", 220},
|
||||
{ "peridot", 63},
|
||||
{ "ruby", 350},
|
||||
{ "sapphire", 285},
|
||||
{ "stibotantalite", 200},
|
||||
{ "tiger eye", 50},
|
||||
{ "topaz", 60},
|
||||
{ "turquoise", 70},
|
||||
{ "taaffeite", 300},
|
||||
{ "zircon", 80},
|
||||
};
|
||||
|
||||
#define NSTONES (sizeof stones / sizeof (STONE))
|
||||
int cNSTONES = NSTONES;
|
||||
|
||||
char *wood[] = {
|
||||
"avocado wood",
|
||||
"balsa",
|
||||
"bamboo",
|
||||
"banyan",
|
||||
"birch",
|
||||
"cedar",
|
||||
"cherry",
|
||||
"cinnibar",
|
||||
"cypress",
|
||||
"dogwood",
|
||||
"driftwood",
|
||||
"ebony",
|
||||
"elm",
|
||||
"eucalyptus",
|
||||
"fall",
|
||||
"hemlock",
|
||||
"holly",
|
||||
"ironwood",
|
||||
"kukui wood",
|
||||
"mahogany",
|
||||
"manzanita",
|
||||
"maple",
|
||||
"oaken",
|
||||
"persimmon wood",
|
||||
"pecan",
|
||||
"pine",
|
||||
"poplar",
|
||||
"redwood",
|
||||
"rosewood",
|
||||
"spruce",
|
||||
"teak",
|
||||
"walnut",
|
||||
"zebrawood",
|
||||
};
|
||||
|
||||
#define NWOOD (sizeof wood / sizeof (char *))
|
||||
int cNWOOD = NWOOD;
|
||||
|
||||
char *metal[] = {
|
||||
"aluminum",
|
||||
"beryllium",
|
||||
"bone",
|
||||
"brass",
|
||||
"bronze",
|
||||
"copper",
|
||||
"electrum",
|
||||
"gold",
|
||||
"iron",
|
||||
"lead",
|
||||
"magnesium",
|
||||
"mercury",
|
||||
"nickel",
|
||||
"pewter",
|
||||
"platinum",
|
||||
"steel",
|
||||
"silver",
|
||||
"silicon",
|
||||
"tin",
|
||||
"titanium",
|
||||
"tungsten",
|
||||
"zinc",
|
||||
};
|
||||
|
||||
#define NMETAL (sizeof metal / sizeof (char *))
|
||||
int cNMETAL = NMETAL;
|
||||
#define MAX3(a,b,c) (a > b ? (a > c ? a : c) : (b > c ? b : c))
|
||||
|
||||
static bool used[MAX3(NCOLORS, NSTONES, NWOOD)];
|
||||
|
||||
/*
|
||||
* init_colors:
|
||||
* Initialize the potion color scheme for this time
|
||||
*/
|
||||
void
|
||||
init_colors()
|
||||
{
|
||||
register int i, j;
|
||||
|
||||
for (i = 0; i < NCOLORS; i++)
|
||||
used[i] = FALSE;
|
||||
for (i = 0; i < MAXPOTIONS; i++)
|
||||
{
|
||||
do
|
||||
j = rnd(NCOLORS);
|
||||
until (!used[j]);
|
||||
used[j] = TRUE;
|
||||
p_colors[i] = rainbow[j];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* init_names:
|
||||
* Generate the names of the various scrolls
|
||||
*/
|
||||
#define MAXNAME 40 /* Max number of characters in a name */
|
||||
|
||||
void
|
||||
init_names()
|
||||
{
|
||||
register int nsyl;
|
||||
register char *cp, *sp;
|
||||
register int i, nwords;
|
||||
|
||||
for (i = 0; i < MAXSCROLLS; i++)
|
||||
{
|
||||
cp = prbuf;
|
||||
nwords = rnd(3) + 2;
|
||||
while (nwords--)
|
||||
{
|
||||
nsyl = rnd(3) + 1;
|
||||
while (nsyl--)
|
||||
{
|
||||
sp = sylls[rnd((sizeof sylls) / (sizeof (char *)))];
|
||||
if (&cp[strlen(sp)] > &prbuf[MAXNAME])
|
||||
break;
|
||||
while (*sp)
|
||||
*cp++ = *sp++;
|
||||
}
|
||||
*cp++ = ' ';
|
||||
}
|
||||
*--cp = '\0';
|
||||
s_names[i] = (char *) malloc((unsigned) strlen(prbuf)+1);
|
||||
strcpy(s_names[i], prbuf);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* init_stones:
|
||||
* Initialize the ring stone setting scheme for this time
|
||||
*/
|
||||
void
|
||||
init_stones()
|
||||
{
|
||||
register int i, j;
|
||||
|
||||
for (i = 0; i < NSTONES; i++)
|
||||
used[i] = FALSE;
|
||||
for (i = 0; i < MAXRINGS; i++)
|
||||
{
|
||||
do
|
||||
j = rnd(NSTONES);
|
||||
until (!used[j]);
|
||||
used[j] = TRUE;
|
||||
r_stones[i] = stones[j].st_name;
|
||||
ring_info[i].oi_worth += stones[j].st_value;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* init_materials:
|
||||
* Initialize the construction materials for wands and staffs
|
||||
*/
|
||||
void
|
||||
init_materials()
|
||||
{
|
||||
register int i, j;
|
||||
register char *str;
|
||||
static bool metused[NMETAL];
|
||||
|
||||
for (i = 0; i < NWOOD; i++)
|
||||
used[i] = FALSE;
|
||||
for (i = 0; i < NMETAL; i++)
|
||||
metused[i] = FALSE;
|
||||
for (i = 0; i < MAXSTICKS; i++)
|
||||
{
|
||||
for (;;)
|
||||
if (rnd(2) == 0)
|
||||
{
|
||||
j = rnd(NMETAL);
|
||||
if (!metused[j])
|
||||
{
|
||||
ws_type[i] = "wand";
|
||||
str = metal[j];
|
||||
metused[j] = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
j = rnd(NWOOD);
|
||||
if (!used[j])
|
||||
{
|
||||
ws_type[i] = "staff";
|
||||
str = wood[j];
|
||||
used[j] = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ws_made[i] = str;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MASTER
|
||||
# define NT NUMTHINGS, "things"
|
||||
# define MP MAXPOTIONS, "potions"
|
||||
# define MS MAXSCROLLS, "scrolls"
|
||||
# define MR MAXRINGS, "rings"
|
||||
# define MWS MAXSTICKS, "sticks"
|
||||
# define MW MAXWEAPONS, "weapons"
|
||||
# define MA MAXARMORS, "armor"
|
||||
#else
|
||||
# define NT NUMTHINGS
|
||||
# define MP MAXPOTIONS
|
||||
# define MS MAXSCROLLS
|
||||
# define MR MAXRINGS
|
||||
# define MWS MAXSTICKS
|
||||
# define MW MAXWEAPONS
|
||||
# define MA MAXARMORS
|
||||
#endif
|
||||
|
||||
/*
|
||||
* sumprobs:
|
||||
* Sum up the probabilities for items appearing
|
||||
*/
|
||||
void
|
||||
sumprobs(struct obj_info *info, int bound
|
||||
#ifdef MASTER
|
||||
, char *name
|
||||
#endif
|
||||
)
|
||||
{
|
||||
#ifdef MASTER
|
||||
struct obj_info *start = info;
|
||||
#endif
|
||||
struct obj_info *endp;
|
||||
|
||||
endp = info + bound;
|
||||
while (++info < endp)
|
||||
info->oi_prob += (info - 1)->oi_prob;
|
||||
#ifdef MASTER
|
||||
badcheck(name, start, bound);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* init_probs:
|
||||
* Initialize the probabilities for the various items
|
||||
*/
|
||||
void
|
||||
init_probs()
|
||||
{
|
||||
sumprobs(things, NT);
|
||||
sumprobs(pot_info, MP);
|
||||
sumprobs(scr_info, MS);
|
||||
sumprobs(ring_info, MR);
|
||||
sumprobs(ws_info, MWS);
|
||||
sumprobs(weap_info, MW);
|
||||
sumprobs(arm_info, MA);
|
||||
}
|
||||
|
||||
#ifdef MASTER
|
||||
/*
|
||||
* badcheck:
|
||||
* Check to see if a series of probabilities sums to 100
|
||||
*/
|
||||
void
|
||||
badcheck(char *name, struct obj_info *info, int bound)
|
||||
{
|
||||
register struct obj_info *end;
|
||||
|
||||
if (info[bound - 1].oi_prob == 100)
|
||||
return;
|
||||
printf("\nBad percentages for %s (bound = %d):\n", name, bound);
|
||||
for (end = &info[bound]; info < end; info++)
|
||||
printf("%3d%% %s\n", info->oi_prob, info->oi_name);
|
||||
printf("[hit RETURN to continue]");
|
||||
fflush(stdout);
|
||||
while (getchar() != '\n')
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* pick_color:
|
||||
* If he is halucinating, pick a random color name and return it,
|
||||
* otherwise return the given color.
|
||||
*/
|
||||
char *
|
||||
pick_color(char *col)
|
||||
{
|
||||
return (on(player, ISHALU) ? rainbow[rnd(NCOLORS)] : col);
|
||||
}
|
||||
323
install-sh
Executable file
323
install-sh
Executable file
@@ -0,0 +1,323 @@
|
||||
#!/bin/sh
|
||||
# install - install a program, script, or datafile
|
||||
|
||||
scriptversion=2005-05-14.22
|
||||
|
||||
# This originates from X11R5 (mit/util/scripts/install.sh), which was
|
||||
# later released in X11R6 (xc/config/util/install.sh) with the
|
||||
# following copyright and license.
|
||||
#
|
||||
# Copyright (C) 1994 X Consortium
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-
|
||||
# TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# Except as contained in this notice, the name of the X Consortium shall not
|
||||
# be used in advertising or otherwise to promote the sale, use or other deal-
|
||||
# ings in this Software without prior written authorization from the X Consor-
|
||||
# tium.
|
||||
#
|
||||
#
|
||||
# FSF changes to this file are in the public domain.
|
||||
#
|
||||
# Calling this script install-sh is preferred over install.sh, to prevent
|
||||
# `make' implicit rules from creating a file called install from it
|
||||
# when there is no Makefile.
|
||||
#
|
||||
# This script is compatible with the BSD install script, but was written
|
||||
# from scratch. It can only install one file at a time, a restriction
|
||||
# shared with many OS's install programs.
|
||||
|
||||
# set DOITPROG to echo to test this script
|
||||
|
||||
# Don't use :- since 4.3BSD and earlier shells don't like it.
|
||||
doit="${DOITPROG-}"
|
||||
|
||||
# put in absolute paths if you don't have them in your path; or use env. vars.
|
||||
|
||||
mvprog="${MVPROG-mv}"
|
||||
cpprog="${CPPROG-cp}"
|
||||
chmodprog="${CHMODPROG-chmod}"
|
||||
chownprog="${CHOWNPROG-chown}"
|
||||
chgrpprog="${CHGRPPROG-chgrp}"
|
||||
stripprog="${STRIPPROG-strip}"
|
||||
rmprog="${RMPROG-rm}"
|
||||
mkdirprog="${MKDIRPROG-mkdir}"
|
||||
|
||||
chmodcmd="$chmodprog 0755"
|
||||
chowncmd=
|
||||
chgrpcmd=
|
||||
stripcmd=
|
||||
rmcmd="$rmprog -f"
|
||||
mvcmd="$mvprog"
|
||||
src=
|
||||
dst=
|
||||
dir_arg=
|
||||
dstarg=
|
||||
no_target_directory=
|
||||
|
||||
usage="Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE
|
||||
or: $0 [OPTION]... SRCFILES... DIRECTORY
|
||||
or: $0 [OPTION]... -t DIRECTORY SRCFILES...
|
||||
or: $0 [OPTION]... -d DIRECTORIES...
|
||||
|
||||
In the 1st form, copy SRCFILE to DSTFILE.
|
||||
In the 2nd and 3rd, copy all SRCFILES to DIRECTORY.
|
||||
In the 4th, create DIRECTORIES.
|
||||
|
||||
Options:
|
||||
-c (ignored)
|
||||
-d create directories instead of installing files.
|
||||
-g GROUP $chgrpprog installed files to GROUP.
|
||||
-m MODE $chmodprog installed files to MODE.
|
||||
-o USER $chownprog installed files to USER.
|
||||
-s $stripprog installed files.
|
||||
-t DIRECTORY install into DIRECTORY.
|
||||
-T report an error if DSTFILE is a directory.
|
||||
--help display this help and exit.
|
||||
--version display version info and exit.
|
||||
|
||||
Environment variables override the default commands:
|
||||
CHGRPPROG CHMODPROG CHOWNPROG CPPROG MKDIRPROG MVPROG RMPROG STRIPPROG
|
||||
"
|
||||
|
||||
while test -n "$1"; do
|
||||
case $1 in
|
||||
-c) shift
|
||||
continue;;
|
||||
|
||||
-d) dir_arg=true
|
||||
shift
|
||||
continue;;
|
||||
|
||||
-g) chgrpcmd="$chgrpprog $2"
|
||||
shift
|
||||
shift
|
||||
continue;;
|
||||
|
||||
--help) echo "$usage"; exit $?;;
|
||||
|
||||
-m) chmodcmd="$chmodprog $2"
|
||||
shift
|
||||
shift
|
||||
continue;;
|
||||
|
||||
-o) chowncmd="$chownprog $2"
|
||||
shift
|
||||
shift
|
||||
continue;;
|
||||
|
||||
-s) stripcmd=$stripprog
|
||||
shift
|
||||
continue;;
|
||||
|
||||
-t) dstarg=$2
|
||||
shift
|
||||
shift
|
||||
continue;;
|
||||
|
||||
-T) no_target_directory=true
|
||||
shift
|
||||
continue;;
|
||||
|
||||
--version) echo "$0 $scriptversion"; exit $?;;
|
||||
|
||||
*) # When -d is used, all remaining arguments are directories to create.
|
||||
# When -t is used, the destination is already specified.
|
||||
test -n "$dir_arg$dstarg" && break
|
||||
# Otherwise, the last argument is the destination. Remove it from $@.
|
||||
for arg
|
||||
do
|
||||
if test -n "$dstarg"; then
|
||||
# $@ is not empty: it contains at least $arg.
|
||||
set fnord "$@" "$dstarg"
|
||||
shift # fnord
|
||||
fi
|
||||
shift # arg
|
||||
dstarg=$arg
|
||||
done
|
||||
break;;
|
||||
esac
|
||||
done
|
||||
|
||||
if test -z "$1"; then
|
||||
if test -z "$dir_arg"; then
|
||||
echo "$0: no input file specified." >&2
|
||||
exit 1
|
||||
fi
|
||||
# It's OK to call `install-sh -d' without argument.
|
||||
# This can happen when creating conditional directories.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for src
|
||||
do
|
||||
# Protect names starting with `-'.
|
||||
case $src in
|
||||
-*) src=./$src ;;
|
||||
esac
|
||||
|
||||
if test -n "$dir_arg"; then
|
||||
dst=$src
|
||||
src=
|
||||
|
||||
if test -d "$dst"; then
|
||||
mkdircmd=:
|
||||
chmodcmd=
|
||||
else
|
||||
mkdircmd=$mkdirprog
|
||||
fi
|
||||
else
|
||||
# Waiting for this to be detected by the "$cpprog $src $dsttmp" command
|
||||
# might cause directories to be created, which would be especially bad
|
||||
# if $src (and thus $dsttmp) contains '*'.
|
||||
if test ! -f "$src" && test ! -d "$src"; then
|
||||
echo "$0: $src does not exist." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if test -z "$dstarg"; then
|
||||
echo "$0: no destination specified." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dst=$dstarg
|
||||
# Protect names starting with `-'.
|
||||
case $dst in
|
||||
-*) dst=./$dst ;;
|
||||
esac
|
||||
|
||||
# If destination is a directory, append the input filename; won't work
|
||||
# if double slashes aren't ignored.
|
||||
if test -d "$dst"; then
|
||||
if test -n "$no_target_directory"; then
|
||||
echo "$0: $dstarg: Is a directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
dst=$dst/`basename "$src"`
|
||||
fi
|
||||
fi
|
||||
|
||||
# This sed command emulates the dirname command.
|
||||
dstdir=`echo "$dst" | sed -e 's,/*$,,;s,[^/]*$,,;s,/*$,,;s,^$,.,'`
|
||||
|
||||
# Make sure that the destination directory exists.
|
||||
|
||||
# Skip lots of stat calls in the usual case.
|
||||
if test ! -d "$dstdir"; then
|
||||
defaultIFS='
|
||||
'
|
||||
IFS="${IFS-$defaultIFS}"
|
||||
|
||||
oIFS=$IFS
|
||||
# Some sh's can't handle IFS=/ for some reason.
|
||||
IFS='%'
|
||||
set x `echo "$dstdir" | sed -e 's@/@%@g' -e 's@^%@/@'`
|
||||
shift
|
||||
IFS=$oIFS
|
||||
|
||||
pathcomp=
|
||||
|
||||
while test $# -ne 0 ; do
|
||||
pathcomp=$pathcomp$1
|
||||
shift
|
||||
if test ! -d "$pathcomp"; then
|
||||
$mkdirprog "$pathcomp"
|
||||
# mkdir can fail with a `File exist' error in case several
|
||||
# install-sh are creating the directory concurrently. This
|
||||
# is OK.
|
||||
test -d "$pathcomp" || exit
|
||||
fi
|
||||
pathcomp=$pathcomp/
|
||||
done
|
||||
fi
|
||||
|
||||
if test -n "$dir_arg"; then
|
||||
$doit $mkdircmd "$dst" \
|
||||
&& { test -z "$chowncmd" || $doit $chowncmd "$dst"; } \
|
||||
&& { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } \
|
||||
&& { test -z "$stripcmd" || $doit $stripcmd "$dst"; } \
|
||||
&& { test -z "$chmodcmd" || $doit $chmodcmd "$dst"; }
|
||||
|
||||
else
|
||||
dstfile=`basename "$dst"`
|
||||
|
||||
# Make a couple of temp file names in the proper directory.
|
||||
dsttmp=$dstdir/_inst.$$_
|
||||
rmtmp=$dstdir/_rm.$$_
|
||||
|
||||
# Trap to clean up those temp files at exit.
|
||||
trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0
|
||||
trap '(exit $?); exit' 1 2 13 15
|
||||
|
||||
# Copy the file name to the temp name.
|
||||
$doit $cpprog "$src" "$dsttmp" &&
|
||||
|
||||
# and set any options; do chmod last to preserve setuid bits.
|
||||
#
|
||||
# If any of these fail, we abort the whole thing. If we want to
|
||||
# ignore errors from any of these, just make sure not to ignore
|
||||
# errors from the above "$doit $cpprog $src $dsttmp" command.
|
||||
#
|
||||
{ test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } \
|
||||
&& { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } \
|
||||
&& { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } \
|
||||
&& { test -z "$chmodcmd" || $doit $chmodcmd "$dsttmp"; } &&
|
||||
|
||||
# Now rename the file to the real destination.
|
||||
{ $doit $mvcmd -f "$dsttmp" "$dstdir/$dstfile" 2>/dev/null \
|
||||
|| {
|
||||
# The rename failed, perhaps because mv can't rename something else
|
||||
# to itself, or perhaps because mv is so ancient that it does not
|
||||
# support -f.
|
||||
|
||||
# Now remove or move aside any old file at destination location.
|
||||
# We try this two ways since rm can't unlink itself on some
|
||||
# systems and the destination file might be busy for other
|
||||
# reasons. In this case, the final cleanup might fail but the new
|
||||
# file should still install successfully.
|
||||
{
|
||||
if test -f "$dstdir/$dstfile"; then
|
||||
$doit $rmcmd -f "$dstdir/$dstfile" 2>/dev/null \
|
||||
|| $doit $mvcmd -f "$dstdir/$dstfile" "$rmtmp" 2>/dev/null \
|
||||
|| {
|
||||
echo "$0: cannot unlink or rename $dstdir/$dstfile" >&2
|
||||
(exit 1); exit 1
|
||||
}
|
||||
else
|
||||
:
|
||||
fi
|
||||
} &&
|
||||
|
||||
# Now rename the file to the real destination.
|
||||
$doit $mvcmd "$dsttmp" "$dstdir/$dstfile"
|
||||
}
|
||||
}
|
||||
fi || { (exit 1); exit 1; }
|
||||
done
|
||||
|
||||
# The final little trick to "correctly" pass the exit status to the exit trap.
|
||||
{
|
||||
(exit 0); exit 0
|
||||
}
|
||||
|
||||
# Local variables:
|
||||
# eval: (add-hook 'write-file-hooks 'time-stamp)
|
||||
# time-stamp-start: "scriptversion="
|
||||
# time-stamp-format: "%:y-%02m-%02d.%02H"
|
||||
# time-stamp-end: "$"
|
||||
# End:
|
||||
277
io.c
Normal file
277
io.c
Normal file
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Various input/output functions
|
||||
*
|
||||
* @(#)io.c 4.32 (Berkeley) 02/05/99
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <curses.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* msg:
|
||||
* Display a message at the top of the screen.
|
||||
*/
|
||||
#define MAXMSG (NUMCOLS - sizeof "--More--")
|
||||
|
||||
static char msgbuf[2*MAXMSG+1];
|
||||
static int newpos = 0;
|
||||
|
||||
/* VARARGS1 */
|
||||
int
|
||||
msg(char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
|
||||
/*
|
||||
* if the string is "", just clear the line
|
||||
*/
|
||||
if (*fmt == '\0')
|
||||
{
|
||||
move(0, 0);
|
||||
clrtoeol();
|
||||
mpos = 0;
|
||||
return ~ESCAPE;
|
||||
}
|
||||
/*
|
||||
* otherwise add to the message and flush it out
|
||||
*/
|
||||
va_start(args, fmt);
|
||||
doadd(fmt, args);
|
||||
va_end(args);
|
||||
return endmsg();
|
||||
}
|
||||
|
||||
/*
|
||||
* addmsg:
|
||||
* Add things to the current message
|
||||
*/
|
||||
/* VARARGS1 */
|
||||
void
|
||||
addmsg(char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
doadd(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
/*
|
||||
* endmsg:
|
||||
* Display a new msg (giving him a chance to see the previous one
|
||||
* if it is up there with the --More--)
|
||||
*/
|
||||
int
|
||||
endmsg()
|
||||
{
|
||||
char ch;
|
||||
|
||||
if (save_msg)
|
||||
strcpy(huh, msgbuf);
|
||||
if (mpos)
|
||||
{
|
||||
look(FALSE);
|
||||
mvaddstr(0, mpos, "--More--");
|
||||
refresh();
|
||||
if (!msg_esc)
|
||||
wait_for(' ');
|
||||
else
|
||||
{
|
||||
while ((ch = readchar()) != ' ')
|
||||
if (ch == ESCAPE)
|
||||
{
|
||||
msgbuf[0] = '\0';
|
||||
mpos = 0;
|
||||
newpos = 0;
|
||||
msgbuf[0] = '\0';
|
||||
return ESCAPE;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* All messages should start with uppercase, except ones that
|
||||
* start with a pack addressing character
|
||||
*/
|
||||
if (islower(msgbuf[0]) && !lower_msg && msgbuf[1] != ')')
|
||||
msgbuf[0] = (char) toupper(msgbuf[0]);
|
||||
mvaddstr(0, 0, msgbuf);
|
||||
clrtoeol();
|
||||
mpos = newpos;
|
||||
newpos = 0;
|
||||
msgbuf[0] = '\0';
|
||||
refresh();
|
||||
return ~ESCAPE;
|
||||
}
|
||||
|
||||
/*
|
||||
* doadd:
|
||||
* Perform an add onto the message buffer
|
||||
*/
|
||||
void
|
||||
doadd(char *fmt, va_list args)
|
||||
{
|
||||
static char buf[MAXSTR];
|
||||
|
||||
/*
|
||||
* Do the printf into buf
|
||||
*/
|
||||
vsprintf(buf, fmt, args);
|
||||
if (strlen(buf) + newpos >= MAXMSG)
|
||||
endmsg();
|
||||
strcat(msgbuf, buf);
|
||||
newpos = (int) strlen(msgbuf);
|
||||
}
|
||||
|
||||
/*
|
||||
* step_ok:
|
||||
* Returns true if it is ok to step on ch
|
||||
*/
|
||||
int
|
||||
step_ok(int ch)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case ' ':
|
||||
case '|':
|
||||
case '-':
|
||||
return FALSE;
|
||||
default:
|
||||
return (!isalpha(ch));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* readchar:
|
||||
* Reads and returns a character, checking for gross input errors
|
||||
*/
|
||||
char
|
||||
readchar()
|
||||
{
|
||||
char ch;
|
||||
|
||||
ch = (char) md_readchar();
|
||||
|
||||
if (ch == 3)
|
||||
{
|
||||
quit(0);
|
||||
return(27);
|
||||
}
|
||||
|
||||
return(ch);
|
||||
}
|
||||
|
||||
/*
|
||||
* status:
|
||||
* Display the important stats line. Keep the cursor where it was.
|
||||
*/
|
||||
void
|
||||
status()
|
||||
{
|
||||
register int oy, ox, temp;
|
||||
static int hpwidth = 0;
|
||||
static int s_hungry = 0;
|
||||
static int s_lvl = 0;
|
||||
static int s_pur = -1;
|
||||
static int s_hp = 0;
|
||||
static int s_arm = 0;
|
||||
static str_t s_str = 0;
|
||||
static int s_exp = 0;
|
||||
static char *state_name[] =
|
||||
{
|
||||
"", "Hungry", "Weak", "Faint"
|
||||
};
|
||||
|
||||
/*
|
||||
* If nothing has changed since the last status, don't
|
||||
* bother.
|
||||
*/
|
||||
temp = (cur_armor != NULL ? cur_armor->o_arm : pstats.s_arm);
|
||||
if (s_hp == pstats.s_hpt && s_exp == pstats.s_exp && s_pur == purse
|
||||
&& s_arm == temp && s_str == pstats.s_str && s_lvl == level
|
||||
&& s_hungry == hungry_state
|
||||
&& !stat_msg
|
||||
)
|
||||
return;
|
||||
|
||||
s_arm = temp;
|
||||
|
||||
getyx(stdscr, oy, ox);
|
||||
if (s_hp != max_hp)
|
||||
{
|
||||
temp = max_hp;
|
||||
s_hp = max_hp;
|
||||
for (hpwidth = 0; temp; hpwidth++)
|
||||
temp /= 10;
|
||||
}
|
||||
|
||||
/*
|
||||
* Save current status
|
||||
*/
|
||||
s_lvl = level;
|
||||
s_pur = purse;
|
||||
s_hp = pstats.s_hpt;
|
||||
s_str = pstats.s_str;
|
||||
s_exp = pstats.s_exp;
|
||||
s_hungry = hungry_state;
|
||||
|
||||
if (stat_msg)
|
||||
{
|
||||
move(0, 0);
|
||||
msg("Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%ld %s",
|
||||
level, purse, hpwidth, pstats.s_hpt, hpwidth, max_hp, pstats.s_str,
|
||||
max_stats.s_str, 10 - s_arm, pstats.s_lvl, pstats.s_exp,
|
||||
state_name[hungry_state]);
|
||||
}
|
||||
else
|
||||
{
|
||||
move(STATLINE, 0);
|
||||
|
||||
printw("Level: %d Gold: %-5d Hp: %*d(%*d) Str: %2d(%d) Arm: %-2d Exp: %d/%d %s",
|
||||
level, purse, hpwidth, pstats.s_hpt, hpwidth, max_hp, pstats.s_str,
|
||||
max_stats.s_str, 10 - s_arm, pstats.s_lvl, pstats.s_exp,
|
||||
state_name[hungry_state]);
|
||||
}
|
||||
|
||||
clrtoeol();
|
||||
move(oy, ox);
|
||||
}
|
||||
|
||||
/*
|
||||
* wait_for
|
||||
* Sit around until the guy types the right key
|
||||
*/
|
||||
void
|
||||
wait_for(int ch)
|
||||
{
|
||||
register char c;
|
||||
|
||||
if (ch == '\n')
|
||||
while ((c = readchar()) != '\n' && c != '\r')
|
||||
continue;
|
||||
else
|
||||
while (readchar() != ch)
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* show_win:
|
||||
* Function used to display a window and wait before returning
|
||||
*/
|
||||
void
|
||||
show_win(char *message)
|
||||
{
|
||||
WINDOW *win;
|
||||
|
||||
win = hw;
|
||||
wmove(win, 0, 0);
|
||||
waddstr(win, message);
|
||||
touchwin(win);
|
||||
wmove(win, hero.y, hero.x);
|
||||
wrefresh(win);
|
||||
wait_for(' ');
|
||||
clearok(curscr, TRUE);
|
||||
touchwin(stdscr);
|
||||
}
|
||||
113
list.c
Normal file
113
list.c
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Functions for dealing with linked lists of goodies
|
||||
*
|
||||
* @(#)list.c 4.12 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
#ifdef MASTER
|
||||
int total = 0; /* total dynamic memory bytes */
|
||||
#endif
|
||||
|
||||
/*
|
||||
* detach:
|
||||
* takes an item out of whatever linked list it might be in
|
||||
*/
|
||||
|
||||
void
|
||||
_detach(THING **list, THING *item)
|
||||
{
|
||||
if (*list == item)
|
||||
*list = next(item);
|
||||
if (prev(item) != NULL)
|
||||
item->l_prev->l_next = next(item);
|
||||
if (next(item) != NULL)
|
||||
item->l_next->l_prev = prev(item);
|
||||
item->l_next = NULL;
|
||||
item->l_prev = NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* _attach:
|
||||
* add an item to the head of a list
|
||||
*/
|
||||
|
||||
void
|
||||
_attach(THING **list, THING *item)
|
||||
{
|
||||
if (*list != NULL)
|
||||
{
|
||||
item->l_next = *list;
|
||||
(*list)->l_prev = item;
|
||||
item->l_prev = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
item->l_next = NULL;
|
||||
item->l_prev = NULL;
|
||||
}
|
||||
*list = item;
|
||||
}
|
||||
|
||||
/*
|
||||
* _free_list:
|
||||
* Throw the whole blamed thing away
|
||||
*/
|
||||
|
||||
void
|
||||
_free_list(THING **ptr)
|
||||
{
|
||||
THING *item;
|
||||
|
||||
while (*ptr != NULL)
|
||||
{
|
||||
item = *ptr;
|
||||
*ptr = next(item);
|
||||
discard(item);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* discard:
|
||||
* Free up an item
|
||||
*/
|
||||
|
||||
void
|
||||
discard(THING *item)
|
||||
{
|
||||
#ifdef MASTER
|
||||
total--;
|
||||
#endif
|
||||
free((char *) item);
|
||||
}
|
||||
|
||||
/*
|
||||
* new_item
|
||||
* Get a new item with a specified size
|
||||
*/
|
||||
THING *
|
||||
new_item()
|
||||
{
|
||||
THING *item;
|
||||
|
||||
#ifdef MASTER
|
||||
if ((item = calloc(1, sizeof *item)) == NULL)
|
||||
msg("ran out of memory after %d items", total);
|
||||
else
|
||||
total++;
|
||||
#else
|
||||
item = calloc(1, sizeof *item);
|
||||
#endif
|
||||
item->l_next = NULL;
|
||||
item->l_prev = NULL;
|
||||
return item;
|
||||
}
|
||||
457
mach_dep.c
Normal file
457
mach_dep.c
Normal file
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* Various installation dependent routines
|
||||
*
|
||||
* @(#)mach_dep.c 4.37 (Berkeley) 05/23/83
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
/*
|
||||
* The various tuneable defines are:
|
||||
*
|
||||
* SCOREFILE Where/if the score file should live.
|
||||
* ALLSCORES Score file is top ten scores, not top ten
|
||||
* players. This is only useful when only a few
|
||||
* people will be playing; otherwise the score file
|
||||
* gets hogged by just a few people.
|
||||
* NUMSCORES Number of scores in the score file (default 10).
|
||||
* NUMNAME String version of NUMSCORES (first character
|
||||
* should be capitalized) (default "Ten").
|
||||
* MAXLOAD What (if any) the maximum load average should be
|
||||
* when people are playing. Since it is divided
|
||||
* by 10, to specify a load limit of 4.0, MAXLOAD
|
||||
* should be "40". If defined, then
|
||||
* LOADAV Should it use it's own routine to get
|
||||
* the load average?
|
||||
* NAMELIST If so, where does the system namelist
|
||||
* hide?
|
||||
* MAXUSERS What (if any) the maximum user count should be
|
||||
* when people are playing. If defined, then
|
||||
* UCOUNT Should it use it's own routine to count
|
||||
* users?
|
||||
* UTMP If so, where does the user list hide?
|
||||
* CHECKTIME How often/if it should check during the game
|
||||
* for high load average.
|
||||
*/
|
||||
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <curses.h>
|
||||
#include "extern.h"
|
||||
|
||||
#define NOOP(x) (x += 0)
|
||||
|
||||
# ifndef NUMSCORES
|
||||
# define NUMSCORES 10
|
||||
# define NUMNAME "Ten"
|
||||
# endif
|
||||
|
||||
unsigned int numscores = NUMSCORES;
|
||||
char *Numname = NUMNAME;
|
||||
|
||||
# ifdef ALLSCORES
|
||||
bool allscore = TRUE;
|
||||
# else /* ALLSCORES */
|
||||
bool allscore = FALSE;
|
||||
# endif /* ALLSCORES */
|
||||
|
||||
#ifdef CHECKTIME
|
||||
static int num_checks; /* times we've gone over in checkout() */
|
||||
#endif /* CHECKTIME */
|
||||
|
||||
/*
|
||||
* init_check:
|
||||
* Check out too see if it is proper to play the game now
|
||||
*/
|
||||
|
||||
void
|
||||
init_check()
|
||||
{
|
||||
#if defined(MAXLOAD) || defined(MAXUSERS)
|
||||
if (too_much())
|
||||
{
|
||||
printf("Sorry, %s, but the system is too loaded now.\n", whoami);
|
||||
printf("Try again later. Meanwhile, why not enjoy a%s %s?\n",
|
||||
vowelstr(fruit), fruit);
|
||||
if (author())
|
||||
printf("However, since you're a good guy, it's up to you\n");
|
||||
else
|
||||
exit(1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* open_score:
|
||||
* Open up the score file for future use
|
||||
*/
|
||||
|
||||
void
|
||||
open_score()
|
||||
{
|
||||
#ifdef SCOREFILE
|
||||
char *scorefile = SCOREFILE;
|
||||
/*
|
||||
* We drop setgid privileges after opening the score file, so subsequent
|
||||
* open()'s will fail. Just reuse the earlier filehandle.
|
||||
*/
|
||||
|
||||
if (scoreboard != NULL) {
|
||||
rewind(scoreboard);
|
||||
return;
|
||||
}
|
||||
|
||||
scoreboard = fopen(scorefile, "r+");
|
||||
|
||||
if ((scoreboard == NULL) && (errno == ENOENT))
|
||||
{
|
||||
scoreboard = fopen(scorefile, "w+");
|
||||
md_chmod(scorefile,0664);
|
||||
}
|
||||
|
||||
if (scoreboard == NULL) {
|
||||
fprintf(stderr, "Could not open %s for writing: %s\n", scorefile, strerror(errno));
|
||||
fflush(stderr);
|
||||
}
|
||||
#else
|
||||
scoreboard = NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* setup:
|
||||
* Get starting setup for all games
|
||||
*/
|
||||
|
||||
void
|
||||
setup()
|
||||
{
|
||||
#ifdef CHECKTIME
|
||||
int checkout();
|
||||
#endif
|
||||
|
||||
#ifdef DUMP
|
||||
md_onsignal_autosave();
|
||||
#else
|
||||
md_onsignal_default();
|
||||
#endif
|
||||
|
||||
#ifdef CHECKTIME
|
||||
md_start_checkout_timer(CHECKTIME*60);
|
||||
num_checks = 0;
|
||||
#endif
|
||||
|
||||
raw(); /* Raw mode */
|
||||
noecho(); /* Echo off */
|
||||
keypad(stdscr,1);
|
||||
getltchars(); /* get the local tty chars */
|
||||
}
|
||||
|
||||
/*
|
||||
* getltchars:
|
||||
* Get the local tty chars for later use
|
||||
*/
|
||||
|
||||
void
|
||||
getltchars()
|
||||
{
|
||||
got_ltc = TRUE;
|
||||
orig_dsusp = md_dsuspchar();
|
||||
md_setdsuspchar( md_suspchar() );
|
||||
}
|
||||
|
||||
/*
|
||||
* resetltchars:
|
||||
* Reset the local tty chars to original values.
|
||||
*/
|
||||
void
|
||||
resetltchars(void)
|
||||
{
|
||||
if (got_ltc) {
|
||||
md_setdsuspchar(orig_dsusp);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* playltchars:
|
||||
* Set local tty chars to the values we use when playing.
|
||||
*/
|
||||
void
|
||||
playltchars(void)
|
||||
{
|
||||
if (got_ltc) {
|
||||
md_setdsuspchar( md_suspchar() );
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* start_score:
|
||||
* Start the scoring sequence
|
||||
*/
|
||||
|
||||
void
|
||||
start_score()
|
||||
{
|
||||
#ifdef CHECKTIME
|
||||
md_stop_checkout_timer();
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* is_symlink:
|
||||
* See if the file has a symbolic link
|
||||
*/
|
||||
bool
|
||||
is_symlink(char *sp)
|
||||
{
|
||||
#ifdef S_IFLNK
|
||||
struct stat sbuf2;
|
||||
|
||||
if (lstat(sp, &sbuf2) < 0)
|
||||
return FALSE;
|
||||
else
|
||||
return ((sbuf2.st_mode & S_IFMT) != S_IFREG);
|
||||
#else
|
||||
NOOP(sp);
|
||||
return FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(MAXLOAD) || defined(MAXUSERS)
|
||||
/*
|
||||
* too_much:
|
||||
* See if the system is being used too much for this game
|
||||
*/
|
||||
bool
|
||||
too_much()
|
||||
{
|
||||
#ifdef MAXLOAD
|
||||
double avec[3];
|
||||
#else
|
||||
int cnt;
|
||||
#endif
|
||||
|
||||
#ifdef MAXLOAD
|
||||
md_loadav(avec);
|
||||
if (avec[1] > (MAXLOAD / 10.0))
|
||||
return TRUE;
|
||||
#endif
|
||||
#ifdef MAXUSERS
|
||||
if (ucount() > MAXUSERS)
|
||||
return TRUE;
|
||||
#endif
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* author:
|
||||
* See if a user is an author of the program
|
||||
*/
|
||||
bool
|
||||
author()
|
||||
{
|
||||
#ifdef MASTER
|
||||
if (wizard)
|
||||
return TRUE;
|
||||
#endif
|
||||
switch (md_getuid())
|
||||
{
|
||||
case -1:
|
||||
return TRUE;
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CHECKTIME
|
||||
/*
|
||||
* checkout:
|
||||
* Check each CHECKTIME seconds to see if the load is too high
|
||||
*/
|
||||
|
||||
checkout(int sig)
|
||||
{
|
||||
static char *msgs[] = {
|
||||
"The load is too high to be playing. Please leave in %0.1f minutes",
|
||||
"Please save your game. You have %0.1f minutes",
|
||||
"Last warning. You have %0.1f minutes to leave",
|
||||
};
|
||||
int checktime;
|
||||
|
||||
if (too_much())
|
||||
{
|
||||
if (author())
|
||||
{
|
||||
num_checks = 1;
|
||||
chmsg("The load is rather high, O exaulted one");
|
||||
}
|
||||
else if (num_checks++ == 3)
|
||||
fatal("Sorry. You took too long. You are dead\n");
|
||||
checktime = (CHECKTIME * 60) / num_checks;
|
||||
chmsg(msgs[num_checks - 1], ((double) checktime / 60.0));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (num_checks)
|
||||
{
|
||||
num_checks = 0;
|
||||
chmsg("The load has dropped back down. You have a reprieve");
|
||||
}
|
||||
checktime = (CHECKTIME * 60);
|
||||
}
|
||||
|
||||
md_start_checkout_timer(checktime);
|
||||
}
|
||||
|
||||
/*
|
||||
* chmsg:
|
||||
* checkout()'s version of msg. If we are in the middle of a
|
||||
* shell, do a printf instead of a msg to a the refresh.
|
||||
*/
|
||||
/* VARARGS1 */
|
||||
|
||||
chmsg(char *fmt, int arg)
|
||||
{
|
||||
if (!in_shell)
|
||||
msg(fmt, arg);
|
||||
else
|
||||
{
|
||||
printf(fmt, arg);
|
||||
putchar('\n');
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef UCOUNT
|
||||
/*
|
||||
* ucount:
|
||||
* count number of users on the system
|
||||
*/
|
||||
#include <utmp.h>
|
||||
|
||||
struct utmp buf;
|
||||
|
||||
int
|
||||
ucount()
|
||||
{
|
||||
struct utmp *up;
|
||||
FILE *utmp;
|
||||
int count;
|
||||
|
||||
if ((utmp = fopen(UTMP, "r")) == NULL)
|
||||
return 0;
|
||||
|
||||
up = &buf;
|
||||
count = 0;
|
||||
|
||||
while (fread(up, 1, sizeof (*up), utmp) > 0)
|
||||
if (buf.ut_name[0] != '\0')
|
||||
count++;
|
||||
fclose(utmp);
|
||||
return count;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* lock_sc:
|
||||
* lock the score file. If it takes too long, ask the user if
|
||||
* they care to wait. Return TRUE if the lock is successful.
|
||||
*/
|
||||
static FILE *lfd = NULL;
|
||||
bool
|
||||
lock_sc()
|
||||
{
|
||||
#if defined(SCOREFILE) && defined(LOCKFILE)
|
||||
int cnt;
|
||||
static struct stat sbuf;
|
||||
char *lockfile = LOCKFILE;
|
||||
|
||||
over:
|
||||
if ((lfd=fopen(lockfile, "w+")) != NULL)
|
||||
return TRUE;
|
||||
for (cnt = 0; cnt < 5; cnt++)
|
||||
{
|
||||
md_sleep(1);
|
||||
if ((lfd=fopen(lockfile, "w+")) != NULL)
|
||||
return TRUE;
|
||||
}
|
||||
if (stat(lockfile, &sbuf) < 0)
|
||||
{
|
||||
lfd=fopen(lockfile, "w+");
|
||||
return TRUE;
|
||||
}
|
||||
if (time(NULL) - sbuf.st_mtime > 10)
|
||||
{
|
||||
if (md_unlink(lockfile) < 0)
|
||||
return FALSE;
|
||||
goto over;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("The score file is very busy. Do you want to wait longer\n");
|
||||
printf("for it to become free so your score can get posted?\n");
|
||||
printf("If so, type \"y\"\n");
|
||||
(void) fgets(prbuf, MAXSTR, stdin);
|
||||
if (prbuf[0] == 'y')
|
||||
for (;;)
|
||||
{
|
||||
if ((lfd=fopen(lockfile, "w+")) != 0)
|
||||
return TRUE;
|
||||
if (stat(lockfile, &sbuf) < 0)
|
||||
{
|
||||
lfd=fopen(lockfile, "w+");
|
||||
return TRUE;
|
||||
}
|
||||
if (time(NULL) - sbuf.st_mtime > 10)
|
||||
{
|
||||
if (md_unlink(lockfile) < 0)
|
||||
return FALSE;
|
||||
}
|
||||
md_sleep(1);
|
||||
}
|
||||
else
|
||||
return FALSE;
|
||||
}
|
||||
#else
|
||||
return TRUE;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* unlock_sc:
|
||||
* Unlock the score file
|
||||
*/
|
||||
|
||||
void
|
||||
unlock_sc()
|
||||
{
|
||||
#if defined(SCOREFILE) && defined(LOCKFILE)
|
||||
if (lfd != NULL)
|
||||
fclose(lfd);
|
||||
lfd = NULL;
|
||||
md_unlink(LOCKFILE);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* flush_type:
|
||||
* Flush typeahead for traps, etc.
|
||||
*/
|
||||
|
||||
void
|
||||
flush_type()
|
||||
{
|
||||
flushinp();
|
||||
}
|
||||
396
main.c
Normal file
396
main.c
Normal file
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*
|
||||
* @(#)main.c 4.22 (Berkeley) 02/05/99
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* main:
|
||||
* The main program, of course
|
||||
*/
|
||||
int
|
||||
main(int argc, char **argv, char **envp)
|
||||
{
|
||||
char *env;
|
||||
int lowtime;
|
||||
|
||||
md_init();
|
||||
|
||||
#ifdef MASTER
|
||||
/*
|
||||
* Check to see if he is a wizard
|
||||
*/
|
||||
if (argc >= 2 && argv[1][0] == '\0')
|
||||
if (strcmp(PASSWD, md_crypt(md_getpass("wizard's password: "), "mT")) == 0)
|
||||
{
|
||||
wizard = TRUE;
|
||||
player.t_flags |= SEEMONST;
|
||||
argv++;
|
||||
argc--;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* get home and options from environment
|
||||
*/
|
||||
|
||||
strncpy(home, md_gethomedir(), MAXSTR);
|
||||
|
||||
strcpy(file_name, home);
|
||||
strcat(file_name, "rogue.save");
|
||||
|
||||
if ((env = getenv("ROGUEOPTS")) != NULL)
|
||||
parse_opts(env);
|
||||
if (env == NULL || whoami[0] == '\0')
|
||||
strucpy(whoami, md_getusername(), (int) strlen(md_getusername()));
|
||||
lowtime = (int) time(NULL);
|
||||
#ifdef MASTER
|
||||
if (wizard && getenv("SEED") != NULL)
|
||||
dnum = atoi(getenv("SEED"));
|
||||
else
|
||||
#endif
|
||||
dnum = lowtime + md_getpid();
|
||||
seed = dnum;
|
||||
|
||||
open_score();
|
||||
|
||||
/*
|
||||
* Drop setuid/setgid after opening the scoreboard file.
|
||||
*/
|
||||
|
||||
md_normaluser();
|
||||
|
||||
/*
|
||||
* check for print-score option
|
||||
*/
|
||||
|
||||
md_normaluser(); /* we drop any setgid/setuid priveldges here */
|
||||
|
||||
if (argc == 2)
|
||||
{
|
||||
if (strcmp(argv[1], "-s") == 0)
|
||||
{
|
||||
noscore = TRUE;
|
||||
score(0, -1, 0);
|
||||
exit(0);
|
||||
}
|
||||
else if (strcmp(argv[1], "-d") == 0)
|
||||
{
|
||||
dnum = rnd(100); /* throw away some rnd()s to break patterns */
|
||||
while (--dnum)
|
||||
rnd(100);
|
||||
purse = rnd(100) + 1;
|
||||
level = rnd(100) + 1;
|
||||
initscr();
|
||||
getltchars();
|
||||
death(death_monst());
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
init_check(); /* check for legal startup */
|
||||
if (argc == 2)
|
||||
if (!restore(argv[1], envp)) /* Note: restore will never return */
|
||||
my_exit(1);
|
||||
#ifdef MASTER
|
||||
if (wizard)
|
||||
printf("Hello %s, welcome to dungeon #%d", whoami, dnum);
|
||||
else
|
||||
#endif
|
||||
printf("Hello %s, just a moment while I dig the dungeon...", whoami);
|
||||
fflush(stdout);
|
||||
|
||||
initscr(); /* Start up cursor package */
|
||||
init_probs(); /* Set up prob tables for objects */
|
||||
init_player(); /* Set up initial player stats */
|
||||
init_names(); /* Set up names of scrolls */
|
||||
init_colors(); /* Set up colors of potions */
|
||||
init_stones(); /* Set up stone settings of rings */
|
||||
init_materials(); /* Set up materials of wands */
|
||||
setup();
|
||||
|
||||
/*
|
||||
* The screen must be at least NUMLINES x NUMCOLS
|
||||
*/
|
||||
if (LINES < NUMLINES || COLS < NUMCOLS)
|
||||
{
|
||||
printf("\nSorry, the screen must be at least %dx%d\n", NUMLINES, NUMCOLS);
|
||||
endwin();
|
||||
my_exit(1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Set up windows
|
||||
*/
|
||||
hw = newwin(LINES, COLS, 0, 0);
|
||||
idlok(stdscr, TRUE);
|
||||
idlok(hw, TRUE);
|
||||
#ifdef MASTER
|
||||
noscore = wizard;
|
||||
#endif
|
||||
new_level(); /* Draw current level */
|
||||
/*
|
||||
* Start up daemons and fuses
|
||||
*/
|
||||
start_daemon(runners, 0, AFTER);
|
||||
start_daemon(doctor, 0, AFTER);
|
||||
fuse(swander, 0, WANDERTIME, AFTER);
|
||||
start_daemon(stomach, 0, AFTER);
|
||||
playit();
|
||||
return(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* endit:
|
||||
* Exit the program abnormally.
|
||||
*/
|
||||
|
||||
void
|
||||
endit(int sig)
|
||||
{
|
||||
NOOP(sig);
|
||||
fatal("Okay, bye bye!\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* fatal:
|
||||
* Exit the program, printing a message.
|
||||
*/
|
||||
|
||||
void
|
||||
fatal(char *s)
|
||||
{
|
||||
mvaddstr(LINES - 2, 0, s);
|
||||
refresh();
|
||||
endwin();
|
||||
my_exit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* rnd:
|
||||
* Pick a very random number.
|
||||
*/
|
||||
int
|
||||
rnd(int range)
|
||||
{
|
||||
return range == 0 ? 0 : abs((int) RN) % range;
|
||||
}
|
||||
|
||||
/*
|
||||
* roll:
|
||||
* Roll a number of dice
|
||||
*/
|
||||
int
|
||||
roll(int number, int sides)
|
||||
{
|
||||
int dtotal = 0;
|
||||
|
||||
while (number--)
|
||||
dtotal += rnd(sides)+1;
|
||||
return dtotal;
|
||||
}
|
||||
|
||||
/*
|
||||
* tstp:
|
||||
* Handle stop and start signals
|
||||
*/
|
||||
|
||||
void
|
||||
tstp(int ignored)
|
||||
{
|
||||
int y, x;
|
||||
int oy, ox;
|
||||
|
||||
NOOP(ignored);
|
||||
|
||||
/*
|
||||
* leave nicely
|
||||
*/
|
||||
getyx(curscr, oy, ox);
|
||||
mvcur(0, COLS - 1, LINES - 1, 0);
|
||||
endwin();
|
||||
resetltchars();
|
||||
fflush(stdout);
|
||||
md_tstpsignal();
|
||||
|
||||
/*
|
||||
* start back up again
|
||||
*/
|
||||
md_tstpresume();
|
||||
raw();
|
||||
noecho();
|
||||
keypad(stdscr,1);
|
||||
playltchars();
|
||||
clearok(curscr, TRUE);
|
||||
wrefresh(curscr);
|
||||
getyx(curscr, y, x);
|
||||
mvcur(y, x, oy, ox);
|
||||
fflush(stdout);
|
||||
curscr->_cury = oy;
|
||||
curscr->_curx = ox;
|
||||
}
|
||||
|
||||
/*
|
||||
* playit:
|
||||
* The main loop of the program. Loop until the game is over,
|
||||
* refreshing things and looking at the proper times.
|
||||
*/
|
||||
|
||||
void
|
||||
playit()
|
||||
{
|
||||
char *opts;
|
||||
|
||||
/*
|
||||
* set up defaults for slow terminals
|
||||
*/
|
||||
|
||||
if (baudrate() <= 1200)
|
||||
{
|
||||
terse = TRUE;
|
||||
jump = TRUE;
|
||||
see_floor = FALSE;
|
||||
}
|
||||
|
||||
if (md_hasclreol())
|
||||
inv_type = INV_CLEAR;
|
||||
|
||||
/*
|
||||
* parse environment declaration of options
|
||||
*/
|
||||
if ((opts = getenv("ROGUEOPTS")) != NULL)
|
||||
parse_opts(opts);
|
||||
|
||||
|
||||
oldpos = hero;
|
||||
oldrp = roomin(&hero);
|
||||
while (playing)
|
||||
command(); /* Command execution */
|
||||
endit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* quit:
|
||||
* Have player make certain, then exit.
|
||||
*/
|
||||
|
||||
void
|
||||
quit(int sig)
|
||||
{
|
||||
int oy, ox;
|
||||
|
||||
NOOP(sig);
|
||||
|
||||
/*
|
||||
* Reset the signal in case we got here via an interrupt
|
||||
*/
|
||||
if (!q_comm)
|
||||
mpos = 0;
|
||||
getyx(curscr, oy, ox);
|
||||
msg("really quit?");
|
||||
if (readchar() == 'y')
|
||||
{
|
||||
signal(SIGINT, leave);
|
||||
clear();
|
||||
mvprintw(LINES - 2, 0, "You quit with %d gold pieces", purse);
|
||||
move(LINES - 1, 0);
|
||||
refresh();
|
||||
score(purse, 1, 0);
|
||||
my_exit(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
move(0, 0);
|
||||
clrtoeol();
|
||||
status();
|
||||
move(oy, ox);
|
||||
refresh();
|
||||
mpos = 0;
|
||||
count = 0;
|
||||
to_death = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* leave:
|
||||
* Leave quickly, but curteously
|
||||
*/
|
||||
|
||||
void
|
||||
leave(int sig)
|
||||
{
|
||||
static char buf[BUFSIZ];
|
||||
|
||||
NOOP(sig);
|
||||
|
||||
setbuf(stdout, buf); /* throw away pending output */
|
||||
|
||||
if (!isendwin())
|
||||
{
|
||||
mvcur(0, COLS - 1, LINES - 1, 0);
|
||||
endwin();
|
||||
}
|
||||
|
||||
putchar('\n');
|
||||
my_exit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* shell:
|
||||
* Let them escape for a while
|
||||
*/
|
||||
|
||||
void
|
||||
shell()
|
||||
{
|
||||
/*
|
||||
* Set the terminal back to original mode
|
||||
*/
|
||||
move(LINES-1, 0);
|
||||
refresh();
|
||||
endwin();
|
||||
resetltchars();
|
||||
putchar('\n');
|
||||
in_shell = TRUE;
|
||||
after = FALSE;
|
||||
fflush(stdout);
|
||||
/*
|
||||
* Fork and do a shell
|
||||
*/
|
||||
md_shellescape();
|
||||
|
||||
printf("\n[Press return to continue]");
|
||||
fflush(stdout);
|
||||
noecho();
|
||||
raw();
|
||||
keypad(stdscr,1);
|
||||
playltchars();
|
||||
in_shell = FALSE;
|
||||
wait_for('\n');
|
||||
clearok(stdscr, TRUE);
|
||||
}
|
||||
|
||||
/*
|
||||
* my_exit:
|
||||
* Leave the process properly
|
||||
*/
|
||||
|
||||
void
|
||||
my_exit(int st)
|
||||
{
|
||||
resetltchars();
|
||||
exit(st);
|
||||
}
|
||||
|
||||
597
misc.c
Normal file
597
misc.c
Normal file
@@ -0,0 +1,597 @@
|
||||
/*
|
||||
* All sorts of miscellaneous routines
|
||||
*
|
||||
* @(#)misc.c 4.66 (Berkeley) 08/06/83
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <curses.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* look:
|
||||
* A quick glance all around the player
|
||||
*/
|
||||
#undef DEBUG
|
||||
|
||||
|
||||
void
|
||||
look(bool wakeup)
|
||||
{
|
||||
int x, y;
|
||||
int ch;
|
||||
THING *tp;
|
||||
PLACE *pp;
|
||||
struct room *rp;
|
||||
int ey, ex;
|
||||
int passcount;
|
||||
char pfl, *fp, pch;
|
||||
int sy, sx, sumhero = 0, diffhero = 0;
|
||||
# ifdef DEBUG
|
||||
static bool done = FALSE;
|
||||
|
||||
if (done)
|
||||
return;
|
||||
done = TRUE;
|
||||
# endif /* DEBUG */
|
||||
passcount = 0;
|
||||
rp = proom;
|
||||
if (!ce(oldpos, hero))
|
||||
{
|
||||
erase_lamp(&oldpos, oldrp);
|
||||
oldpos = hero;
|
||||
oldrp = rp;
|
||||
}
|
||||
ey = hero.y + 1;
|
||||
ex = hero.x + 1;
|
||||
sx = hero.x - 1;
|
||||
sy = hero.y - 1;
|
||||
if (door_stop && !firstmove && running)
|
||||
{
|
||||
sumhero = hero.y + hero.x;
|
||||
diffhero = hero.y - hero.x;
|
||||
}
|
||||
pp = INDEX(hero.y, hero.x);
|
||||
pch = pp->p_ch;
|
||||
pfl = pp->p_flags;
|
||||
|
||||
for (y = sy; y <= ey; y++)
|
||||
if (y > 0 && y < NUMLINES - 1) for (x = sx; x <= ex; x++)
|
||||
{
|
||||
if (x < 0 || x >= NUMCOLS)
|
||||
continue;
|
||||
if (!on(player, ISBLIND))
|
||||
{
|
||||
if (y == hero.y && x == hero.x)
|
||||
continue;
|
||||
}
|
||||
|
||||
pp = INDEX(y, x);
|
||||
ch = pp->p_ch;
|
||||
if (ch == ' ') /* nothing need be done with a ' ' */
|
||||
continue;
|
||||
fp = &pp->p_flags;
|
||||
if (pch != DOOR && ch != DOOR)
|
||||
if ((pfl & F_PASS) != (*fp & F_PASS))
|
||||
continue;
|
||||
if (((*fp & F_PASS) || ch == DOOR) &&
|
||||
((pfl & F_PASS) || pch == DOOR))
|
||||
{
|
||||
if (hero.x != x && hero.y != y &&
|
||||
!step_ok(chat(y, hero.x)) && !step_ok(chat(hero.y, x)))
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((tp = pp->p_monst) == NULL)
|
||||
ch = trip_ch(y, x, ch);
|
||||
else
|
||||
if (on(player, SEEMONST) && on(*tp, ISINVIS))
|
||||
{
|
||||
if (door_stop && !firstmove)
|
||||
running = FALSE;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (wakeup)
|
||||
wake_monster(y, x);
|
||||
if (see_monst(tp))
|
||||
{
|
||||
if (on(player, ISHALU))
|
||||
ch = rnd(26) + 'A';
|
||||
else
|
||||
ch = tp->t_disguise;
|
||||
}
|
||||
}
|
||||
if (on(player, ISBLIND) && (y != hero.y || x != hero.x))
|
||||
continue;
|
||||
|
||||
move(y, x);
|
||||
|
||||
if ((proom->r_flags & ISDARK) && !see_floor && ch == FLOOR)
|
||||
ch = ' ';
|
||||
|
||||
if (tp != NULL || ch != CCHAR( inch() ))
|
||||
addch(ch);
|
||||
|
||||
if (door_stop && !firstmove && running)
|
||||
{
|
||||
switch (runch)
|
||||
{
|
||||
case 'h':
|
||||
if (x == ex)
|
||||
continue;
|
||||
when 'j':
|
||||
if (y == sy)
|
||||
continue;
|
||||
when 'k':
|
||||
if (y == ey)
|
||||
continue;
|
||||
when 'l':
|
||||
if (x == sx)
|
||||
continue;
|
||||
when 'y':
|
||||
if ((y + x) - sumhero >= 1)
|
||||
continue;
|
||||
when 'u':
|
||||
if ((y - x) - diffhero >= 1)
|
||||
continue;
|
||||
when 'n':
|
||||
if ((y + x) - sumhero <= -1)
|
||||
continue;
|
||||
when 'b':
|
||||
if ((y - x) - diffhero <= -1)
|
||||
continue;
|
||||
}
|
||||
switch (ch)
|
||||
{
|
||||
case DOOR:
|
||||
if (x == hero.x || y == hero.y)
|
||||
running = FALSE;
|
||||
break;
|
||||
case PASSAGE:
|
||||
if (x == hero.x || y == hero.y)
|
||||
passcount++;
|
||||
break;
|
||||
case FLOOR:
|
||||
case '|':
|
||||
case '-':
|
||||
case ' ':
|
||||
break;
|
||||
default:
|
||||
running = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (door_stop && !firstmove && passcount > 1)
|
||||
running = FALSE;
|
||||
if (!running || !jump)
|
||||
mvaddch(hero.y, hero.x, PLAYER);
|
||||
# ifdef DEBUG
|
||||
done = FALSE;
|
||||
# endif /* DEBUG */
|
||||
}
|
||||
|
||||
/*
|
||||
* trip_ch:
|
||||
* Return the character appropriate for this space, taking into
|
||||
* account whether or not the player is tripping.
|
||||
*/
|
||||
int
|
||||
trip_ch(int y, int x, int ch)
|
||||
{
|
||||
if (on(player, ISHALU) && after)
|
||||
switch (ch)
|
||||
{
|
||||
case FLOOR:
|
||||
case ' ':
|
||||
case PASSAGE:
|
||||
case '-':
|
||||
case '|':
|
||||
case DOOR:
|
||||
case TRAP:
|
||||
break;
|
||||
default:
|
||||
if (y != stairs.y || x != stairs.x || !seenstairs)
|
||||
ch = rnd_thing();
|
||||
break;
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
/*
|
||||
* erase_lamp:
|
||||
* Erase the area shown by a lamp in a dark room.
|
||||
*/
|
||||
|
||||
void
|
||||
erase_lamp(coord *pos, struct room *rp)
|
||||
{
|
||||
int y, x, ey, sy, ex;
|
||||
|
||||
if (!(see_floor && (rp->r_flags & (ISGONE|ISDARK)) == ISDARK
|
||||
&& !on(player,ISBLIND)))
|
||||
return;
|
||||
|
||||
ey = pos->y + 1;
|
||||
ex = pos->x + 1;
|
||||
sy = pos->y - 1;
|
||||
for (x = pos->x - 1; x <= ex; x++)
|
||||
for (y = sy; y <= ey; y++)
|
||||
{
|
||||
if (y == hero.y && x == hero.x)
|
||||
continue;
|
||||
move(y, x);
|
||||
if (inch() == FLOOR)
|
||||
addch(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* show_floor:
|
||||
* Should we show the floor in her room at this time?
|
||||
*/
|
||||
bool
|
||||
show_floor()
|
||||
{
|
||||
if ((proom->r_flags & (ISGONE|ISDARK)) == ISDARK && !on(player, ISBLIND))
|
||||
return see_floor;
|
||||
else
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* find_obj:
|
||||
* Find the unclaimed object at y, x
|
||||
*/
|
||||
THING *
|
||||
find_obj(int y, int x)
|
||||
{
|
||||
THING *obj;
|
||||
|
||||
for (obj = lvl_obj; obj != NULL; obj = next(obj))
|
||||
{
|
||||
if (obj->o_pos.y == y && obj->o_pos.x == x)
|
||||
return obj;
|
||||
}
|
||||
#ifdef MASTER
|
||||
sprintf(prbuf, "Non-object %d,%d", y, x);
|
||||
msg(prbuf);
|
||||
return NULL;
|
||||
#else
|
||||
/* NOTREACHED */
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* eat:
|
||||
* She wants to eat something, so let her try
|
||||
*/
|
||||
|
||||
void
|
||||
eat()
|
||||
{
|
||||
THING *obj;
|
||||
|
||||
if ((obj = get_item("eat", FOOD)) == NULL)
|
||||
return;
|
||||
if (obj->o_type != FOOD)
|
||||
{
|
||||
if (!terse)
|
||||
msg("ugh, you would get ill if you ate that");
|
||||
else
|
||||
msg("that's Inedible!");
|
||||
return;
|
||||
}
|
||||
if (food_left < 0)
|
||||
food_left = 0;
|
||||
if ((food_left += HUNGERTIME - 200 + rnd(400)) > STOMACHSIZE)
|
||||
food_left = STOMACHSIZE;
|
||||
hungry_state = 0;
|
||||
if (obj == cur_weapon)
|
||||
cur_weapon = NULL;
|
||||
if (obj->o_which == 1)
|
||||
msg("my, that was a yummy %s", fruit);
|
||||
else
|
||||
if (rnd(100) > 70)
|
||||
{
|
||||
pstats.s_exp++;
|
||||
msg("%s, this food tastes awful", choose_str("bummer", "yuk"));
|
||||
check_level();
|
||||
}
|
||||
else
|
||||
msg("%s, that tasted good", choose_str("oh, wow", "yum"));
|
||||
leave_pack(obj, FALSE, FALSE);
|
||||
}
|
||||
|
||||
/*
|
||||
* check_level:
|
||||
* Check to see if the guy has gone up a level.
|
||||
*/
|
||||
|
||||
void
|
||||
check_level()
|
||||
{
|
||||
int i, add, olevel;
|
||||
|
||||
for (i = 0; e_levels[i] != 0; i++)
|
||||
if (e_levels[i] > pstats.s_exp)
|
||||
break;
|
||||
i++;
|
||||
olevel = pstats.s_lvl;
|
||||
pstats.s_lvl = i;
|
||||
if (i > olevel)
|
||||
{
|
||||
add = roll(i - olevel, 10);
|
||||
max_hp += add;
|
||||
pstats.s_hpt += add;
|
||||
msg("welcome to level %d", i);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* chg_str:
|
||||
* used to modify the playes strength. It keeps track of the
|
||||
* highest it has been, just in case
|
||||
*/
|
||||
|
||||
void
|
||||
chg_str(int amt)
|
||||
{
|
||||
auto str_t comp;
|
||||
|
||||
if (amt == 0)
|
||||
return;
|
||||
add_str(&pstats.s_str, amt);
|
||||
comp = pstats.s_str;
|
||||
if (ISRING(LEFT, R_ADDSTR))
|
||||
add_str(&comp, -cur_ring[LEFT]->o_arm);
|
||||
if (ISRING(RIGHT, R_ADDSTR))
|
||||
add_str(&comp, -cur_ring[RIGHT]->o_arm);
|
||||
if (comp > max_stats.s_str)
|
||||
max_stats.s_str = comp;
|
||||
}
|
||||
|
||||
/*
|
||||
* add_str:
|
||||
* Perform the actual add, checking upper and lower bound limits
|
||||
*/
|
||||
void
|
||||
add_str(str_t *sp, int amt)
|
||||
{
|
||||
if ((*sp += amt) < 3)
|
||||
*sp = 3;
|
||||
else if (*sp > 31)
|
||||
*sp = 31;
|
||||
}
|
||||
|
||||
/*
|
||||
* add_haste:
|
||||
* Add a haste to the player
|
||||
*/
|
||||
bool
|
||||
add_haste(bool potion)
|
||||
{
|
||||
if (on(player, ISHASTE))
|
||||
{
|
||||
no_command += rnd(8);
|
||||
player.t_flags &= ~(ISRUN|ISHASTE);
|
||||
extinguish(nohaste);
|
||||
msg("you faint from exhaustion");
|
||||
return FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
player.t_flags |= ISHASTE;
|
||||
if (potion)
|
||||
fuse(nohaste, 0, rnd(4)+4, AFTER);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* aggravate:
|
||||
* Aggravate all the monsters on this level
|
||||
*/
|
||||
|
||||
void
|
||||
aggravate()
|
||||
{
|
||||
THING *mp;
|
||||
|
||||
for (mp = mlist; mp != NULL; mp = next(mp))
|
||||
runto(&mp->t_pos);
|
||||
}
|
||||
|
||||
/*
|
||||
* vowelstr:
|
||||
* For printfs: if string starts with a vowel, return "n" for an
|
||||
* "an".
|
||||
*/
|
||||
char *
|
||||
vowelstr(char *str)
|
||||
{
|
||||
switch (*str)
|
||||
{
|
||||
case 'a': case 'A':
|
||||
case 'e': case 'E':
|
||||
case 'i': case 'I':
|
||||
case 'o': case 'O':
|
||||
case 'u': case 'U':
|
||||
return "n";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* is_current:
|
||||
* See if the object is one of the currently used items
|
||||
*/
|
||||
bool
|
||||
is_current(THING *obj)
|
||||
{
|
||||
if (obj == NULL)
|
||||
return FALSE;
|
||||
if (obj == cur_armor || obj == cur_weapon || obj == cur_ring[LEFT]
|
||||
|| obj == cur_ring[RIGHT])
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("That's already ");
|
||||
msg("in use");
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* get_dir:
|
||||
* Set up the direction co_ordinate for use in varios "prefix"
|
||||
* commands
|
||||
*/
|
||||
bool
|
||||
get_dir()
|
||||
{
|
||||
char *prompt;
|
||||
bool gotit;
|
||||
static coord last_delt= {0,0};
|
||||
|
||||
if (again && last_dir != '\0')
|
||||
{
|
||||
delta.y = last_delt.y;
|
||||
delta.x = last_delt.x;
|
||||
dir_ch = last_dir;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!terse)
|
||||
msg(prompt = "which direction? ");
|
||||
else
|
||||
prompt = "direction: ";
|
||||
do
|
||||
{
|
||||
gotit = TRUE;
|
||||
switch (dir_ch = readchar())
|
||||
{
|
||||
case 'h': case'H': delta.y = 0; delta.x = -1;
|
||||
when 'j': case'J': delta.y = 1; delta.x = 0;
|
||||
when 'k': case'K': delta.y = -1; delta.x = 0;
|
||||
when 'l': case'L': delta.y = 0; delta.x = 1;
|
||||
when 'y': case'Y': delta.y = -1; delta.x = -1;
|
||||
when 'u': case'U': delta.y = -1; delta.x = 1;
|
||||
when 'b': case'B': delta.y = 1; delta.x = -1;
|
||||
when 'n': case'N': delta.y = 1; delta.x = 1;
|
||||
when ESCAPE: last_dir = '\0'; reset_last(); return FALSE;
|
||||
otherwise:
|
||||
mpos = 0;
|
||||
msg(prompt);
|
||||
gotit = FALSE;
|
||||
}
|
||||
} until (gotit);
|
||||
if (isupper(dir_ch))
|
||||
dir_ch = (char) tolower(dir_ch);
|
||||
last_dir = dir_ch;
|
||||
last_delt.y = delta.y;
|
||||
last_delt.x = delta.x;
|
||||
}
|
||||
if (on(player, ISHUH) && rnd(5) == 0)
|
||||
do
|
||||
{
|
||||
delta.y = rnd(3) - 1;
|
||||
delta.x = rnd(3) - 1;
|
||||
} while (delta.y == 0 && delta.x == 0);
|
||||
mpos = 0;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* sign:
|
||||
* Return the sign of the number
|
||||
*/
|
||||
int
|
||||
sign(int nm)
|
||||
{
|
||||
if (nm < 0)
|
||||
return -1;
|
||||
else
|
||||
return (nm > 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* spread:
|
||||
* Give a spread around a given number (+/- 20%)
|
||||
*/
|
||||
int
|
||||
spread(int nm)
|
||||
{
|
||||
return nm - nm / 20 + rnd(nm / 10);
|
||||
}
|
||||
|
||||
/*
|
||||
* call_it:
|
||||
* Call an object something after use.
|
||||
*/
|
||||
|
||||
void
|
||||
call_it(struct obj_info *info)
|
||||
{
|
||||
if (info->oi_know)
|
||||
{
|
||||
if (info->oi_guess)
|
||||
{
|
||||
free(info->oi_guess);
|
||||
info->oi_guess = NULL;
|
||||
}
|
||||
}
|
||||
else if (!info->oi_guess)
|
||||
{
|
||||
msg(terse ? "call it: " : "what do you want to call it? ");
|
||||
if (get_str(prbuf, stdscr) == NORM)
|
||||
{
|
||||
if (info->oi_guess != NULL)
|
||||
free(info->oi_guess);
|
||||
info->oi_guess = malloc((unsigned int) strlen(prbuf) + 1);
|
||||
strcpy(info->oi_guess, prbuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* rnd_thing:
|
||||
* Pick a random thing appropriate for this level
|
||||
*/
|
||||
char
|
||||
rnd_thing()
|
||||
{
|
||||
int i;
|
||||
static char thing_list[] = {
|
||||
POTION, SCROLL, RING, STICK, FOOD, WEAPON, ARMOR, STAIRS, GOLD, AMULET
|
||||
};
|
||||
|
||||
if (level >= AMULETLEVEL)
|
||||
i = rnd(sizeof thing_list / sizeof (char));
|
||||
else
|
||||
i = rnd(sizeof thing_list / sizeof (char) - 1);
|
||||
return thing_list[i];
|
||||
}
|
||||
|
||||
/*
|
||||
str str:
|
||||
* Choose the first or second string depending on whether it the
|
||||
* player is tripping
|
||||
*/
|
||||
char *
|
||||
choose_str(char *ts, char *ns)
|
||||
{
|
||||
return (on(player, ISHALU) ? ts : ns);
|
||||
}
|
||||
252
monsters.c
Normal file
252
monsters.c
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* File with various monster functions in it
|
||||
*
|
||||
* @(#)monsters.c 4.46 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include <string.h>
|
||||
#include "rogue.h"
|
||||
#include <ctype.h>
|
||||
|
||||
/*
|
||||
* List of monsters in rough order of vorpalness
|
||||
*/
|
||||
static char lvl_mons[] = {
|
||||
'K', 'E', 'B', 'S', 'H', 'I', 'R', 'O', 'Z', 'L', 'C', 'Q', 'A',
|
||||
'N', 'Y', 'F', 'T', 'W', 'P', 'X', 'U', 'M', 'V', 'G', 'J', 'D'
|
||||
};
|
||||
|
||||
static char wand_mons[] = {
|
||||
'K', 'E', 'B', 'S', 'H', 0, 'R', 'O', 'Z', 0, 'C', 'Q', 'A',
|
||||
0, 'Y', 0, 'T', 'W', 'P', 0, 'U', 'M', 'V', 'G', 'J', 0
|
||||
};
|
||||
|
||||
/*
|
||||
* randmonster:
|
||||
* Pick a monster to show up. The lower the level,
|
||||
* the meaner the monster.
|
||||
*/
|
||||
char
|
||||
randmonster(bool wander)
|
||||
{
|
||||
int d;
|
||||
char *mons;
|
||||
|
||||
mons = (wander ? wand_mons : lvl_mons);
|
||||
do
|
||||
{
|
||||
d = level + (rnd(10) - 6);
|
||||
if (d < 0)
|
||||
d = rnd(5);
|
||||
if (d > 25)
|
||||
d = rnd(5) + 21;
|
||||
} while (mons[d] == 0);
|
||||
return mons[d];
|
||||
}
|
||||
|
||||
/*
|
||||
* new_monster:
|
||||
* Pick a new monster and add it to the list
|
||||
*/
|
||||
|
||||
void
|
||||
new_monster(THING *tp, char type, coord *cp)
|
||||
{
|
||||
struct monster *mp;
|
||||
int lev_add;
|
||||
|
||||
if ((lev_add = level - AMULETLEVEL) < 0)
|
||||
lev_add = 0;
|
||||
attach(mlist, tp);
|
||||
tp->t_type = type;
|
||||
tp->t_disguise = type;
|
||||
tp->t_pos = *cp;
|
||||
move(cp->y, cp->x);
|
||||
tp->t_oldch = CCHAR( inch() );
|
||||
tp->t_room = roomin(cp);
|
||||
moat(cp->y, cp->x) = tp;
|
||||
mp = &monsters[tp->t_type-'A'];
|
||||
tp->t_stats.s_lvl = mp->m_stats.s_lvl + lev_add;
|
||||
tp->t_stats.s_maxhp = tp->t_stats.s_hpt = roll(tp->t_stats.s_lvl, 8);
|
||||
tp->t_stats.s_arm = mp->m_stats.s_arm - lev_add;
|
||||
strcpy(tp->t_stats.s_dmg,mp->m_stats.s_dmg);
|
||||
tp->t_stats.s_str = mp->m_stats.s_str;
|
||||
tp->t_stats.s_exp = mp->m_stats.s_exp + lev_add * 10 + exp_add(tp);
|
||||
tp->t_flags = mp->m_flags;
|
||||
if (level > 29)
|
||||
tp->t_flags |= ISHASTE;
|
||||
tp->t_turn = TRUE;
|
||||
tp->t_pack = NULL;
|
||||
if (ISWEARING(R_AGGR))
|
||||
runto(cp);
|
||||
if (type == 'X')
|
||||
tp->t_disguise = rnd_thing();
|
||||
}
|
||||
|
||||
/*
|
||||
* expadd:
|
||||
* Experience to add for this monster's level/hit points
|
||||
*/
|
||||
int
|
||||
exp_add(THING *tp)
|
||||
{
|
||||
int mod;
|
||||
|
||||
if (tp->t_stats.s_lvl == 1)
|
||||
mod = tp->t_stats.s_maxhp / 8;
|
||||
else
|
||||
mod = tp->t_stats.s_maxhp / 6;
|
||||
if (tp->t_stats.s_lvl > 9)
|
||||
mod *= 20;
|
||||
else if (tp->t_stats.s_lvl > 6)
|
||||
mod *= 4;
|
||||
return mod;
|
||||
}
|
||||
|
||||
/*
|
||||
* wanderer:
|
||||
* Create a new wandering monster and aim it at the player
|
||||
*/
|
||||
|
||||
void
|
||||
wanderer()
|
||||
{
|
||||
THING *tp;
|
||||
static coord cp;
|
||||
|
||||
tp = new_item();
|
||||
do
|
||||
{
|
||||
find_floor((struct room *) NULL, &cp, FALSE, TRUE);
|
||||
} while (roomin(&cp) == proom);
|
||||
new_monster(tp, randmonster(TRUE), &cp);
|
||||
if (on(player, SEEMONST))
|
||||
{
|
||||
standout();
|
||||
if (!on(player, ISHALU))
|
||||
addch(tp->t_type);
|
||||
else
|
||||
addch(rnd(26) + 'A');
|
||||
standend();
|
||||
}
|
||||
runto(&tp->t_pos);
|
||||
#ifdef MASTER
|
||||
if (wizard)
|
||||
msg("started a wandering %s", monsters[tp->t_type-'A'].m_name);
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* wake_monster:
|
||||
* What to do when the hero steps next to a monster
|
||||
*/
|
||||
THING *
|
||||
wake_monster(int y, int x)
|
||||
{
|
||||
THING *tp;
|
||||
struct room *rp;
|
||||
char ch, *mname;
|
||||
|
||||
#ifdef MASTER
|
||||
if ((tp = moat(y, x)) == NULL)
|
||||
msg("can't find monster in wake_monster");
|
||||
#else
|
||||
tp = moat(y, x);
|
||||
if (tp == NULL)
|
||||
endwin(), abort();
|
||||
#endif
|
||||
ch = tp->t_type;
|
||||
/*
|
||||
* Every time he sees mean monster, it might start chasing him
|
||||
*/
|
||||
if (!on(*tp, ISRUN) && rnd(3) != 0 && on(*tp, ISMEAN) && !on(*tp, ISHELD)
|
||||
&& !ISWEARING(R_STEALTH) && !on(player, ISLEVIT))
|
||||
{
|
||||
tp->t_dest = &hero;
|
||||
tp->t_flags |= ISRUN;
|
||||
}
|
||||
if (ch == 'M' && !on(player, ISBLIND) && !on(player, ISHALU)
|
||||
&& !on(*tp, ISFOUND) && !on(*tp, ISCANC) && on(*tp, ISRUN))
|
||||
{
|
||||
rp = proom;
|
||||
if ((rp != NULL && !(rp->r_flags & ISDARK))
|
||||
|| dist(y, x, hero.y, hero.x) < LAMPDIST)
|
||||
{
|
||||
tp->t_flags |= ISFOUND;
|
||||
if (!save(VS_MAGIC))
|
||||
{
|
||||
if (on(player, ISHUH))
|
||||
lengthen(unconfuse, spread(HUHDURATION));
|
||||
else
|
||||
fuse(unconfuse, 0, spread(HUHDURATION), AFTER);
|
||||
player.t_flags |= ISHUH;
|
||||
mname = set_mname(tp);
|
||||
addmsg("%s", mname);
|
||||
if (strcmp(mname, "it") != 0)
|
||||
addmsg("'");
|
||||
msg("s gaze has confused you");
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Let greedy ones guard gold
|
||||
*/
|
||||
if (on(*tp, ISGREED) && !on(*tp, ISRUN))
|
||||
{
|
||||
tp->t_flags |= ISRUN;
|
||||
if (proom->r_goldval)
|
||||
tp->t_dest = &proom->r_gold;
|
||||
else
|
||||
tp->t_dest = &hero;
|
||||
}
|
||||
return tp;
|
||||
}
|
||||
|
||||
/*
|
||||
* give_pack:
|
||||
* Give a pack to a monster if it deserves one
|
||||
*/
|
||||
|
||||
void
|
||||
give_pack(THING *tp)
|
||||
{
|
||||
if (level >= max_level && rnd(100) < monsters[tp->t_type-'A'].m_carry)
|
||||
attach(tp->t_pack, new_thing());
|
||||
}
|
||||
|
||||
/*
|
||||
* save_throw:
|
||||
* See if a creature save against something
|
||||
*/
|
||||
int
|
||||
save_throw(int which, THING *tp)
|
||||
{
|
||||
int need;
|
||||
|
||||
need = 14 + which - tp->t_stats.s_lvl / 2;
|
||||
return (roll(1, 20) >= need);
|
||||
}
|
||||
|
||||
/*
|
||||
* save:
|
||||
* See if he saves against various nasty things
|
||||
*/
|
||||
int
|
||||
save(int which)
|
||||
{
|
||||
if (which == VS_MAGIC)
|
||||
{
|
||||
if (ISRING(LEFT, R_PROTECT))
|
||||
which -= cur_ring[LEFT]->o_arm;
|
||||
if (ISRING(RIGHT, R_PROTECT))
|
||||
which -= cur_ring[RIGHT]->o_arm;
|
||||
}
|
||||
return save_throw(which, &player);
|
||||
}
|
||||
425
move.c
Normal file
425
move.c
Normal file
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* hero movement commands
|
||||
*
|
||||
* @(#)move.c 4.49 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include <ctype.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* used to hold the new hero position
|
||||
*/
|
||||
|
||||
coord nh;
|
||||
|
||||
/*
|
||||
* do_run:
|
||||
* Start the hero running
|
||||
*/
|
||||
|
||||
void
|
||||
do_run(char ch)
|
||||
{
|
||||
running = TRUE;
|
||||
after = FALSE;
|
||||
runch = ch;
|
||||
}
|
||||
|
||||
/*
|
||||
* do_move:
|
||||
* Check to see that a move is legal. If it is handle the
|
||||
* consequences (fighting, picking up, etc.)
|
||||
*/
|
||||
|
||||
void
|
||||
do_move(int dy, int dx)
|
||||
{
|
||||
char ch, fl;
|
||||
|
||||
firstmove = FALSE;
|
||||
if (no_move)
|
||||
{
|
||||
no_move--;
|
||||
msg("you are still stuck in the bear trap");
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* Do a confused move (maybe)
|
||||
*/
|
||||
if (on(player, ISHUH) && rnd(5) != 0)
|
||||
{
|
||||
nh = *rndmove(&player);
|
||||
if (ce(nh, hero))
|
||||
{
|
||||
after = FALSE;
|
||||
running = FALSE;
|
||||
to_death = FALSE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
over:
|
||||
nh.y = hero.y + dy;
|
||||
nh.x = hero.x + dx;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if he tried to move off the screen or make an illegal
|
||||
* diagonal move, and stop him if he did.
|
||||
*/
|
||||
if (nh.x < 0 || nh.x >= NUMCOLS || nh.y <= 0 || nh.y >= NUMLINES - 1)
|
||||
goto hit_bound;
|
||||
if (!diag_ok(&hero, &nh))
|
||||
{
|
||||
after = FALSE;
|
||||
running = FALSE;
|
||||
return;
|
||||
}
|
||||
if (running && ce(hero, nh))
|
||||
after = running = FALSE;
|
||||
fl = flat(nh.y, nh.x);
|
||||
ch = winat(nh.y, nh.x);
|
||||
if (!(fl & F_REAL) && ch == FLOOR)
|
||||
{
|
||||
if (!on(player, ISLEVIT))
|
||||
{
|
||||
chat(nh.y, nh.x) = ch = TRAP;
|
||||
flat(nh.y, nh.x) |= F_REAL;
|
||||
}
|
||||
}
|
||||
else if (on(player, ISHELD) && ch != 'F')
|
||||
{
|
||||
msg("you are being held");
|
||||
return;
|
||||
}
|
||||
switch (ch)
|
||||
{
|
||||
case ' ':
|
||||
case '|':
|
||||
case '-':
|
||||
hit_bound:
|
||||
if (passgo && running && (proom->r_flags & ISGONE)
|
||||
&& !on(player, ISBLIND))
|
||||
{
|
||||
bool b1, b2;
|
||||
|
||||
switch (runch)
|
||||
{
|
||||
case 'h':
|
||||
case 'l':
|
||||
b1 = (bool)(hero.y != 1 && turn_ok(hero.y - 1, hero.x));
|
||||
b2 = (bool)(hero.y != NUMLINES - 2 && turn_ok(hero.y + 1, hero.x));
|
||||
if (!(b1 ^ b2))
|
||||
break;
|
||||
if (b1)
|
||||
{
|
||||
runch = 'k';
|
||||
dy = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
runch = 'j';
|
||||
dy = 1;
|
||||
}
|
||||
dx = 0;
|
||||
turnref();
|
||||
goto over;
|
||||
case 'j':
|
||||
case 'k':
|
||||
b1 = (bool)(hero.x != 0 && turn_ok(hero.y, hero.x - 1));
|
||||
b2 = (bool)(hero.x != NUMCOLS - 1 && turn_ok(hero.y, hero.x + 1));
|
||||
if (!(b1 ^ b2))
|
||||
break;
|
||||
if (b1)
|
||||
{
|
||||
runch = 'h';
|
||||
dx = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
runch = 'l';
|
||||
dx = 1;
|
||||
}
|
||||
dy = 0;
|
||||
turnref();
|
||||
goto over;
|
||||
}
|
||||
}
|
||||
running = FALSE;
|
||||
after = FALSE;
|
||||
break;
|
||||
case DOOR:
|
||||
running = FALSE;
|
||||
if (flat(hero.y, hero.x) & F_PASS)
|
||||
enter_room(&nh);
|
||||
goto move_stuff;
|
||||
case TRAP:
|
||||
ch = be_trapped(&nh);
|
||||
if (ch == T_DOOR || ch == T_TELEP)
|
||||
return;
|
||||
goto move_stuff;
|
||||
case PASSAGE:
|
||||
/*
|
||||
* when you're in a corridor, you don't know if you're in
|
||||
* a maze room or not, and there ain't no way to find out
|
||||
* if you're leaving a maze room, so it is necessary to
|
||||
* always recalculate proom.
|
||||
*/
|
||||
proom = roomin(&hero);
|
||||
goto move_stuff;
|
||||
case FLOOR:
|
||||
if (!(fl & F_REAL))
|
||||
be_trapped(&hero);
|
||||
goto move_stuff;
|
||||
case STAIRS:
|
||||
seenstairs = TRUE;
|
||||
/* FALLTHROUGH */
|
||||
default:
|
||||
running = FALSE;
|
||||
if (isupper(ch) || moat(nh.y, nh.x))
|
||||
fight(&nh, cur_weapon, FALSE);
|
||||
else
|
||||
{
|
||||
if (ch != STAIRS)
|
||||
take = ch;
|
||||
move_stuff:
|
||||
mvaddch(hero.y, hero.x, floor_at());
|
||||
if ((fl & F_PASS) && chat(oldpos.y, oldpos.x) == DOOR)
|
||||
leave_room(&nh);
|
||||
hero = nh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* turn_ok:
|
||||
* Decide whether it is legal to turn onto the given space
|
||||
*/
|
||||
bool
|
||||
turn_ok(int y, int x)
|
||||
{
|
||||
PLACE *pp;
|
||||
|
||||
pp = INDEX(y, x);
|
||||
return (pp->p_ch == DOOR
|
||||
|| (pp->p_flags & (F_REAL|F_PASS)) == (F_REAL|F_PASS));
|
||||
}
|
||||
|
||||
/*
|
||||
* turnref:
|
||||
* Decide whether to refresh at a passage turning or not
|
||||
*/
|
||||
|
||||
void
|
||||
turnref()
|
||||
{
|
||||
PLACE *pp;
|
||||
|
||||
pp = INDEX(hero.y, hero.x);
|
||||
if (!(pp->p_flags & F_SEEN))
|
||||
{
|
||||
if (jump)
|
||||
{
|
||||
leaveok(stdscr, TRUE);
|
||||
refresh();
|
||||
leaveok(stdscr, FALSE);
|
||||
}
|
||||
pp->p_flags |= F_SEEN;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* door_open:
|
||||
* Called to illuminate a room. If it is dark, remove anything
|
||||
* that might move.
|
||||
*/
|
||||
|
||||
void
|
||||
door_open(struct room *rp)
|
||||
{
|
||||
int y, x;
|
||||
|
||||
if (!(rp->r_flags & ISGONE))
|
||||
for (y = rp->r_pos.y; y < rp->r_pos.y + rp->r_max.y; y++)
|
||||
for (x = rp->r_pos.x; x < rp->r_pos.x + rp->r_max.x; x++)
|
||||
if (isupper(winat(y, x)))
|
||||
wake_monster(y, x);
|
||||
}
|
||||
|
||||
/*
|
||||
* be_trapped:
|
||||
* The guy stepped on a trap.... Make him pay.
|
||||
*/
|
||||
char
|
||||
be_trapped(coord *tc)
|
||||
{
|
||||
PLACE *pp;
|
||||
THING *arrow;
|
||||
char tr;
|
||||
|
||||
if (on(player, ISLEVIT))
|
||||
return T_RUST; /* anything that's not a door or teleport */
|
||||
running = FALSE;
|
||||
count = FALSE;
|
||||
pp = INDEX(tc->y, tc->x);
|
||||
pp->p_ch = TRAP;
|
||||
tr = pp->p_flags & F_TMASK;
|
||||
pp->p_flags |= F_SEEN;
|
||||
switch (tr)
|
||||
{
|
||||
case T_DOOR:
|
||||
level++;
|
||||
new_level();
|
||||
msg("you fell into a trap!");
|
||||
when T_BEAR:
|
||||
no_move += BEARTIME;
|
||||
msg("you are caught in a bear trap");
|
||||
when T_MYST:
|
||||
switch(rnd(11))
|
||||
{
|
||||
case 0: msg("you are suddenly in a parallel dimension");
|
||||
when 1: msg("the light in here suddenly seems %s", rainbow[rnd(cNCOLORS)]);
|
||||
when 2: msg("you feel a sting in the side of your neck");
|
||||
when 3: msg("multi-colored lines swirl around you, then fade");
|
||||
when 4: msg("a %s light flashes in your eyes", rainbow[rnd(cNCOLORS)]);
|
||||
when 5: msg("a spike shoots past your ear!");
|
||||
when 6: msg("%s sparks dance across your armor", rainbow[rnd(cNCOLORS)]);
|
||||
when 7: msg("you suddenly feel very thirsty");
|
||||
when 8: msg("you feel time speed up suddenly");
|
||||
when 9: msg("time now seems to be going slower");
|
||||
when 10: msg("you pack turns %s!", rainbow[rnd(cNCOLORS)]);
|
||||
}
|
||||
when T_SLEEP:
|
||||
no_command += SLEEPTIME;
|
||||
player.t_flags &= ~ISRUN;
|
||||
msg("a strange white mist envelops you and you fall asleep");
|
||||
when T_ARROW:
|
||||
if (swing(pstats.s_lvl - 1, pstats.s_arm, 1))
|
||||
{
|
||||
pstats.s_hpt -= roll(1, 6);
|
||||
if (pstats.s_hpt <= 0)
|
||||
{
|
||||
msg("an arrow killed you");
|
||||
death('a');
|
||||
}
|
||||
else
|
||||
msg("oh no! An arrow shot you");
|
||||
}
|
||||
else
|
||||
{
|
||||
arrow = new_item();
|
||||
init_weapon(arrow, ARROW);
|
||||
arrow->o_count = 1;
|
||||
arrow->o_pos = hero;
|
||||
fall(arrow, FALSE);
|
||||
msg("an arrow shoots past you");
|
||||
}
|
||||
when T_TELEP:
|
||||
/*
|
||||
* since the hero's leaving, look() won't put a TRAP
|
||||
* down for us, so we have to do it ourself
|
||||
*/
|
||||
teleport();
|
||||
mvaddch(tc->y, tc->x, TRAP);
|
||||
when T_DART:
|
||||
if (!swing(pstats.s_lvl+1, pstats.s_arm, 1))
|
||||
msg("a small dart whizzes by your ear and vanishes");
|
||||
else
|
||||
{
|
||||
pstats.s_hpt -= roll(1, 4);
|
||||
if (pstats.s_hpt <= 0)
|
||||
{
|
||||
msg("a poisoned dart killed you");
|
||||
death('d');
|
||||
}
|
||||
if (!ISWEARING(R_SUSTSTR) && !save(VS_POISON))
|
||||
chg_str(-1);
|
||||
msg("a small dart just hit you in the shoulder");
|
||||
}
|
||||
when T_RUST:
|
||||
msg("a gush of water hits you on the head");
|
||||
rust_armor(cur_armor);
|
||||
}
|
||||
flush_type();
|
||||
return tr;
|
||||
}
|
||||
|
||||
/*
|
||||
* rndmove:
|
||||
* Move in a random direction if the monster/person is confused
|
||||
*/
|
||||
coord *
|
||||
rndmove(THING *who)
|
||||
{
|
||||
THING *obj;
|
||||
int x, y;
|
||||
char ch;
|
||||
static coord ret; /* what we will be returning */
|
||||
|
||||
y = ret.y = who->t_pos.y + rnd(3) - 1;
|
||||
x = ret.x = who->t_pos.x + rnd(3) - 1;
|
||||
/*
|
||||
* Now check to see if that's a legal move. If not, don't move.
|
||||
* (I.e., bump into the wall or whatever)
|
||||
*/
|
||||
if (y == who->t_pos.y && x == who->t_pos.x)
|
||||
return &ret;
|
||||
if (!diag_ok(&who->t_pos, &ret))
|
||||
goto bad;
|
||||
else
|
||||
{
|
||||
ch = winat(y, x);
|
||||
if (!step_ok(ch))
|
||||
goto bad;
|
||||
if (ch == SCROLL)
|
||||
{
|
||||
for (obj = lvl_obj; obj != NULL; obj = next(obj))
|
||||
if (y == obj->o_pos.y && x == obj->o_pos.x)
|
||||
break;
|
||||
if (obj != NULL && obj->o_which == S_SCARE)
|
||||
goto bad;
|
||||
}
|
||||
}
|
||||
return &ret;
|
||||
|
||||
bad:
|
||||
ret = who->t_pos;
|
||||
return &ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* rust_armor:
|
||||
* Rust the given armor, if it is a legal kind to rust, and we
|
||||
* aren't wearing a magic ring.
|
||||
*/
|
||||
|
||||
void
|
||||
rust_armor(THING *arm)
|
||||
{
|
||||
if (arm == NULL || arm->o_type != ARMOR || arm->o_which == LEATHER ||
|
||||
arm->o_arm >= 9)
|
||||
return;
|
||||
|
||||
if ((arm->o_flags & ISPROT) || ISWEARING(R_SUSTARM))
|
||||
{
|
||||
if (!to_death)
|
||||
msg("the rust vanishes instantly");
|
||||
}
|
||||
else
|
||||
{
|
||||
arm->o_arm++;
|
||||
if (!terse)
|
||||
msg("your armor appears to be weaker now. Oh my!");
|
||||
else
|
||||
msg("your armor weakens");
|
||||
}
|
||||
}
|
||||
231
new_level.c
Normal file
231
new_level.c
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* new_level:
|
||||
* Dig and draw a new level
|
||||
*
|
||||
* @(#)new_level.c 4.38 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include <string.h>
|
||||
#include "rogue.h"
|
||||
|
||||
#define TREAS_ROOM 20 /* one chance in TREAS_ROOM for a treasure room */
|
||||
#define MAXTREAS 10 /* maximum number of treasures in a treasure room */
|
||||
#define MINTREAS 2 /* minimum number of treasures in a treasure room */
|
||||
|
||||
void
|
||||
new_level()
|
||||
{
|
||||
THING *tp;
|
||||
PLACE *pp;
|
||||
char *sp;
|
||||
int i;
|
||||
|
||||
player.t_flags &= ~ISHELD; /* unhold when you go down just in case */
|
||||
if (level > max_level)
|
||||
max_level = level;
|
||||
/*
|
||||
* Clean things off from last level
|
||||
*/
|
||||
for (pp = places; pp < &places[MAXCOLS*MAXLINES]; pp++)
|
||||
{
|
||||
pp->p_ch = ' ';
|
||||
pp->p_flags = F_REAL;
|
||||
pp->p_monst = NULL;
|
||||
}
|
||||
clear();
|
||||
/*
|
||||
* Free up the monsters on the last level
|
||||
*/
|
||||
for (tp = mlist; tp != NULL; tp = next(tp))
|
||||
free_list(tp->t_pack);
|
||||
free_list(mlist);
|
||||
/*
|
||||
* Throw away stuff left on the previous level (if anything)
|
||||
*/
|
||||
free_list(lvl_obj);
|
||||
do_rooms(); /* Draw rooms */
|
||||
do_passages(); /* Draw passages */
|
||||
no_food++;
|
||||
put_things(); /* Place objects (if any) */
|
||||
/*
|
||||
* Place the traps
|
||||
*/
|
||||
if (rnd(10) < level)
|
||||
{
|
||||
ntraps = rnd(level / 4) + 1;
|
||||
if (ntraps > MAXTRAPS)
|
||||
ntraps = MAXTRAPS;
|
||||
i = ntraps;
|
||||
while (i--)
|
||||
{
|
||||
/*
|
||||
* not only wouldn't it be NICE to have traps in mazes
|
||||
* (not that we care about being nice), since the trap
|
||||
* number is stored where the passage number is, we
|
||||
* can't actually do it.
|
||||
*/
|
||||
do
|
||||
{
|
||||
find_floor((struct room *) NULL, &stairs, FALSE, FALSE);
|
||||
} while (chat(stairs.y, stairs.x) != FLOOR);
|
||||
sp = &flat(stairs.y, stairs.x);
|
||||
*sp &= ~F_REAL;
|
||||
*sp |= rnd(NTRAPS);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Place the staircase down.
|
||||
*/
|
||||
find_floor((struct room *) NULL, &stairs, FALSE, FALSE);
|
||||
chat(stairs.y, stairs.x) = STAIRS;
|
||||
seenstairs = FALSE;
|
||||
|
||||
for (tp = mlist; tp != NULL; tp = next(tp))
|
||||
tp->t_room = roomin(&tp->t_pos);
|
||||
|
||||
find_floor((struct room *) NULL, &hero, FALSE, TRUE);
|
||||
enter_room(&hero);
|
||||
mvaddch(hero.y, hero.x, PLAYER);
|
||||
if (on(player, SEEMONST))
|
||||
turn_see(FALSE);
|
||||
if (on(player, ISHALU))
|
||||
visuals();
|
||||
}
|
||||
|
||||
/*
|
||||
* rnd_room:
|
||||
* Pick a room that is really there
|
||||
*/
|
||||
int
|
||||
rnd_room()
|
||||
{
|
||||
int rm;
|
||||
|
||||
do
|
||||
{
|
||||
rm = rnd(MAXROOMS);
|
||||
} while (rooms[rm].r_flags & ISGONE);
|
||||
return rm;
|
||||
}
|
||||
|
||||
/*
|
||||
* put_things:
|
||||
* Put potions and scrolls on this level
|
||||
*/
|
||||
|
||||
void
|
||||
put_things()
|
||||
{
|
||||
int i;
|
||||
THING *obj;
|
||||
|
||||
/*
|
||||
* Once you have found the amulet, the only way to get new stuff is
|
||||
* go down into the dungeon.
|
||||
*/
|
||||
if (amulet && level < max_level)
|
||||
return;
|
||||
/*
|
||||
* check for treasure rooms, and if so, put it in.
|
||||
*/
|
||||
if (rnd(TREAS_ROOM) == 0)
|
||||
treas_room();
|
||||
/*
|
||||
* Do MAXOBJ attempts to put things on a level
|
||||
*/
|
||||
for (i = 0; i < MAXOBJ; i++)
|
||||
if (rnd(100) < 36)
|
||||
{
|
||||
/*
|
||||
* Pick a new object and link it in the list
|
||||
*/
|
||||
obj = new_thing();
|
||||
attach(lvl_obj, obj);
|
||||
/*
|
||||
* Put it somewhere
|
||||
*/
|
||||
find_floor((struct room *) NULL, &obj->o_pos, FALSE, FALSE);
|
||||
chat(obj->o_pos.y, obj->o_pos.x) = (char) obj->o_type;
|
||||
}
|
||||
/*
|
||||
* If he is really deep in the dungeon and he hasn't found the
|
||||
* amulet yet, put it somewhere on the ground
|
||||
*/
|
||||
if (level >= AMULETLEVEL && !amulet)
|
||||
{
|
||||
obj = new_item();
|
||||
attach(lvl_obj, obj);
|
||||
obj->o_hplus = 0;
|
||||
obj->o_dplus = 0;
|
||||
strncpy(obj->o_damage,"0x0",sizeof(obj->o_damage));
|
||||
strncpy(obj->o_hurldmg,"0x0",sizeof(obj->o_hurldmg));
|
||||
obj->o_arm = 11;
|
||||
obj->o_type = AMULET;
|
||||
/*
|
||||
* Put it somewhere
|
||||
*/
|
||||
find_floor((struct room *) NULL, &obj->o_pos, FALSE, FALSE);
|
||||
chat(obj->o_pos.y, obj->o_pos.x) = AMULET;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* treas_room:
|
||||
* Add a treasure room
|
||||
*/
|
||||
#define MAXTRIES 10 /* max number of tries to put down a monster */
|
||||
|
||||
|
||||
void
|
||||
treas_room()
|
||||
{
|
||||
int nm;
|
||||
THING *tp;
|
||||
struct room *rp;
|
||||
int spots, num_monst;
|
||||
static coord mp;
|
||||
|
||||
rp = &rooms[rnd_room()];
|
||||
spots = (rp->r_max.y - 2) * (rp->r_max.x - 2) - MINTREAS;
|
||||
if (spots > (MAXTREAS - MINTREAS))
|
||||
spots = (MAXTREAS - MINTREAS);
|
||||
num_monst = nm = rnd(spots) + MINTREAS;
|
||||
while (nm--)
|
||||
{
|
||||
find_floor(rp, &mp, 2 * MAXTRIES, FALSE);
|
||||
tp = new_thing();
|
||||
tp->o_pos = mp;
|
||||
attach(lvl_obj, tp);
|
||||
chat(mp.y, mp.x) = (char) tp->o_type;
|
||||
}
|
||||
|
||||
/*
|
||||
* fill up room with monsters from the next level down
|
||||
*/
|
||||
|
||||
if ((nm = rnd(spots) + MINTREAS) < num_monst + 2)
|
||||
nm = num_monst + 2;
|
||||
spots = (rp->r_max.y - 2) * (rp->r_max.x - 2);
|
||||
if (nm > spots)
|
||||
nm = spots;
|
||||
level++;
|
||||
while (nm--)
|
||||
{
|
||||
spots = 0;
|
||||
if (find_floor(rp, &mp, MAXTRIES, TRUE))
|
||||
{
|
||||
tp = new_item();
|
||||
new_monster(tp, randmonster(FALSE), &mp);
|
||||
tp->t_flags |= ISMEAN; /* no sloughers in THIS room */
|
||||
give_pack(tp);
|
||||
}
|
||||
}
|
||||
level--;
|
||||
}
|
||||
501
options.c
Normal file
501
options.c
Normal file
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
* This file has all the code for the option command. I would rather
|
||||
* this command were not necessary, but it is the only way to keep the
|
||||
* wolves off of my back.
|
||||
*
|
||||
* @(#)options.c 4.24 (Berkeley) 05/10/83
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <curses.h>
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
#include "rogue.h"
|
||||
|
||||
#define EQSTR(a, b, c) (strncmp(a, b, c) == 0)
|
||||
|
||||
#define NUM_OPTS (sizeof optlist / sizeof (OPTION))
|
||||
|
||||
/*
|
||||
* description of an option and what to do with it
|
||||
*/
|
||||
struct optstruct {
|
||||
char *o_name; /* option name */
|
||||
char *o_prompt; /* prompt for interactive entry */
|
||||
void *o_opt; /* pointer to thing to set */
|
||||
/* function to print value */
|
||||
void (*o_putfunc)(void *opt);
|
||||
/* function to get value interactively */
|
||||
int (*o_getfunc)(void *opt, WINDOW *win);
|
||||
};
|
||||
|
||||
typedef struct optstruct OPTION;
|
||||
|
||||
void pr_optname(OPTION *op);
|
||||
|
||||
OPTION optlist[] = {
|
||||
{"terse", "Terse output",
|
||||
&terse, put_bool, get_bool },
|
||||
{"flush", "Flush typeahead during battle",
|
||||
&fight_flush, put_bool, get_bool },
|
||||
{"jump", "Show position only at end of run",
|
||||
&jump, put_bool, get_bool },
|
||||
{"seefloor", "Show the lamp-illuminated floor",
|
||||
&see_floor, put_bool, get_sf },
|
||||
{"passgo", "Follow turnings in passageways",
|
||||
&passgo, put_bool, get_bool },
|
||||
{"tombstone", "Print out tombstone when killed",
|
||||
&tombstone, put_bool, get_bool },
|
||||
{"inven", "Inventory style",
|
||||
&inv_type, put_inv_t, get_inv_t },
|
||||
{"name", "Name",
|
||||
whoami, put_str, get_str },
|
||||
{"fruit", "Fruit",
|
||||
fruit, put_str, get_str },
|
||||
{"file", "Save file",
|
||||
file_name, put_str, get_str }
|
||||
};
|
||||
|
||||
/*
|
||||
* option:
|
||||
* Print and then set options from the terminal
|
||||
*/
|
||||
|
||||
void
|
||||
option()
|
||||
{
|
||||
OPTION *op;
|
||||
int retval;
|
||||
|
||||
wclear(hw);
|
||||
/*
|
||||
* Display current values of options
|
||||
*/
|
||||
for (op = optlist; op <= &optlist[NUM_OPTS-1]; op++)
|
||||
{
|
||||
pr_optname(op);
|
||||
(*op->o_putfunc)(op->o_opt);
|
||||
waddch(hw, '\n');
|
||||
}
|
||||
/*
|
||||
* Set values
|
||||
*/
|
||||
wmove(hw, 0, 0);
|
||||
for (op = optlist; op <= &optlist[NUM_OPTS-1]; op++)
|
||||
{
|
||||
pr_optname(op);
|
||||
retval = (*op->o_getfunc)(op->o_opt, hw);
|
||||
if (retval)
|
||||
{
|
||||
if (retval == QUIT)
|
||||
break;
|
||||
else if (op > optlist) { /* MINUS */
|
||||
wmove(hw, (int)(op - optlist) - 1, 0);
|
||||
op -= 2;
|
||||
}
|
||||
else /* trying to back up beyond the top */
|
||||
{
|
||||
putchar('\007');
|
||||
wmove(hw, 0, 0);
|
||||
op--;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Switch back to original screen
|
||||
*/
|
||||
wmove(hw, LINES - 1, 0);
|
||||
waddstr(hw, "--Press space to continue--");
|
||||
wrefresh(hw);
|
||||
wait_for(' ');
|
||||
clearok(curscr, TRUE);
|
||||
touchwin(stdscr);
|
||||
after = FALSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* pr_optname:
|
||||
* Print out the option name prompt
|
||||
*/
|
||||
|
||||
void
|
||||
pr_optname(OPTION *op)
|
||||
{
|
||||
wprintw(hw, "%s (\"%s\"): ", op->o_prompt, op->o_name);
|
||||
}
|
||||
|
||||
/*
|
||||
* put_bool
|
||||
* Put out a boolean
|
||||
*/
|
||||
|
||||
void
|
||||
put_bool(void *b)
|
||||
{
|
||||
waddstr(hw, *(bool *) b ? "True" : "False");
|
||||
}
|
||||
|
||||
/*
|
||||
* put_str:
|
||||
* Put out a string
|
||||
*/
|
||||
|
||||
void
|
||||
put_str(void *str)
|
||||
{
|
||||
waddstr(hw, (char *) str);
|
||||
}
|
||||
|
||||
/*
|
||||
* put_inv_t:
|
||||
* Put out an inventory type
|
||||
*/
|
||||
|
||||
void
|
||||
put_inv_t(void *ip)
|
||||
{
|
||||
waddstr(hw, inv_t_name[*(int *) ip]);
|
||||
}
|
||||
|
||||
/*
|
||||
* get_bool:
|
||||
* Allow changing a boolean option and print it out
|
||||
*/
|
||||
int
|
||||
get_bool(void *vp, WINDOW *win)
|
||||
{
|
||||
bool *bp = (bool *) vp;
|
||||
int oy, ox;
|
||||
bool op_bad;
|
||||
|
||||
op_bad = TRUE;
|
||||
getyx(win, oy, ox);
|
||||
waddstr(win, *bp ? "True" : "False");
|
||||
while (op_bad)
|
||||
{
|
||||
wmove(win, oy, ox);
|
||||
wrefresh(win);
|
||||
switch (readchar())
|
||||
{
|
||||
case 't':
|
||||
case 'T':
|
||||
*bp = TRUE;
|
||||
op_bad = FALSE;
|
||||
break;
|
||||
case 'f':
|
||||
case 'F':
|
||||
*bp = FALSE;
|
||||
op_bad = FALSE;
|
||||
break;
|
||||
case '\n':
|
||||
case '\r':
|
||||
op_bad = FALSE;
|
||||
break;
|
||||
case ESCAPE:
|
||||
return QUIT;
|
||||
case '-':
|
||||
return MINUS;
|
||||
default:
|
||||
wmove(win, oy, ox + 10);
|
||||
waddstr(win, "(T or F)");
|
||||
}
|
||||
}
|
||||
wmove(win, oy, ox);
|
||||
waddstr(win, *bp ? "True" : "False");
|
||||
waddch(win, '\n');
|
||||
return NORM;
|
||||
}
|
||||
|
||||
/*
|
||||
* get_sf:
|
||||
* Change value and handle transition problems from see_floor to
|
||||
* !see_floor.
|
||||
*/
|
||||
int
|
||||
get_sf(void *vp, WINDOW *win)
|
||||
{
|
||||
bool *bp = (bool *) vp;
|
||||
bool was_sf;
|
||||
int retval;
|
||||
|
||||
was_sf = see_floor;
|
||||
retval = get_bool(bp, win);
|
||||
if (retval == QUIT) return(QUIT);
|
||||
if (was_sf != see_floor)
|
||||
{
|
||||
if (!see_floor) {
|
||||
see_floor = TRUE;
|
||||
erase_lamp(&hero, proom);
|
||||
see_floor = FALSE;
|
||||
}
|
||||
else
|
||||
look(FALSE);
|
||||
}
|
||||
return(NORM);
|
||||
}
|
||||
|
||||
/*
|
||||
* get_str:
|
||||
* Set a string option
|
||||
*/
|
||||
#define MAXINP 50 /* max string to read from terminal or environment */
|
||||
|
||||
int
|
||||
get_str(void *vopt, WINDOW *win)
|
||||
{
|
||||
char *opt = (char *) vopt;
|
||||
char *sp;
|
||||
int oy, ox;
|
||||
int i;
|
||||
signed char c;
|
||||
static char buf[MAXSTR];
|
||||
|
||||
getyx(win, oy, ox);
|
||||
wrefresh(win);
|
||||
/*
|
||||
* loop reading in the string, and put it in a temporary buffer
|
||||
*/
|
||||
for (sp = buf; (c = readchar()) != '\n' && c != '\r' && c != ESCAPE;
|
||||
wclrtoeol(win), wrefresh(win))
|
||||
{
|
||||
if (c == -1)
|
||||
continue;
|
||||
else if (c == erasechar()) /* process erase character */
|
||||
{
|
||||
if (sp > buf)
|
||||
{
|
||||
sp--;
|
||||
for (i = (int) strlen(unctrl(*sp)); i; i--)
|
||||
waddch(win, '\b');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (c == killchar()) /* process kill character */
|
||||
{
|
||||
sp = buf;
|
||||
wmove(win, oy, ox);
|
||||
continue;
|
||||
}
|
||||
else if (sp == buf)
|
||||
{
|
||||
if (c == '-' && win != stdscr)
|
||||
break;
|
||||
else if (c == '~')
|
||||
{
|
||||
strcpy(buf, home);
|
||||
waddstr(win, home);
|
||||
sp += strlen(home);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (sp >= &buf[MAXINP] || !(isprint(c) || c == ' '))
|
||||
putchar(CTRL('G'));
|
||||
else
|
||||
{
|
||||
*sp++ = c;
|
||||
waddstr(win, unctrl(c));
|
||||
}
|
||||
}
|
||||
*sp = '\0';
|
||||
if (sp > buf) /* only change option if something has been typed */
|
||||
strucpy(opt, buf, (int) strlen(buf));
|
||||
mvwprintw(win, oy, ox, "%s\n", opt);
|
||||
wrefresh(win);
|
||||
if (win == stdscr)
|
||||
mpos += (int)(sp - buf);
|
||||
if (c == '-')
|
||||
return MINUS;
|
||||
else if (c == ESCAPE)
|
||||
return QUIT;
|
||||
else
|
||||
return NORM;
|
||||
}
|
||||
|
||||
/*
|
||||
* get_inv_t
|
||||
* Get an inventory type name
|
||||
*/
|
||||
int
|
||||
get_inv_t(void *vp, WINDOW *win)
|
||||
{
|
||||
int *ip = (int *) vp;
|
||||
int oy, ox;
|
||||
bool op_bad;
|
||||
|
||||
op_bad = TRUE;
|
||||
getyx(win, oy, ox);
|
||||
waddstr(win, inv_t_name[*ip]);
|
||||
while (op_bad)
|
||||
{
|
||||
wmove(win, oy, ox);
|
||||
wrefresh(win);
|
||||
switch (readchar())
|
||||
{
|
||||
case 'o':
|
||||
case 'O':
|
||||
*ip = INV_OVER;
|
||||
op_bad = FALSE;
|
||||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
*ip = INV_SLOW;
|
||||
op_bad = FALSE;
|
||||
break;
|
||||
case 'c':
|
||||
case 'C':
|
||||
*ip = INV_CLEAR;
|
||||
op_bad = FALSE;
|
||||
break;
|
||||
case '\n':
|
||||
case '\r':
|
||||
op_bad = FALSE;
|
||||
break;
|
||||
case ESCAPE:
|
||||
return QUIT;
|
||||
case '-':
|
||||
return MINUS;
|
||||
default:
|
||||
wmove(win, oy, ox + 15);
|
||||
waddstr(win, "(O, S, or C)");
|
||||
}
|
||||
}
|
||||
mvwprintw(win, oy, ox, "%s\n", inv_t_name[*ip]);
|
||||
return NORM;
|
||||
}
|
||||
|
||||
|
||||
#ifdef MASTER
|
||||
/*
|
||||
* get_num:
|
||||
* Get a numeric option
|
||||
*/
|
||||
int
|
||||
get_num(void *vp, WINDOW *win)
|
||||
{
|
||||
short *opt = (short *) vp;
|
||||
int i;
|
||||
static char buf[MAXSTR];
|
||||
|
||||
if ((i = get_str(buf, win)) == NORM)
|
||||
*opt = (short) atoi(buf);
|
||||
return i;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* parse_opts:
|
||||
* Parse options from string, usually taken from the environment.
|
||||
* The string is a series of comma seperated values, with booleans
|
||||
* being stated as "name" (true) or "noname" (false), and strings
|
||||
* being "name=....", with the string being defined up to a comma
|
||||
* or the end of the entire option string.
|
||||
*/
|
||||
|
||||
void
|
||||
parse_opts(char *str)
|
||||
{
|
||||
char *sp;
|
||||
OPTION *op;
|
||||
int len;
|
||||
char **i;
|
||||
char *start;
|
||||
|
||||
while (*str)
|
||||
{
|
||||
/*
|
||||
* Get option name
|
||||
*/
|
||||
for (sp = str; isalpha(*sp); sp++)
|
||||
continue;
|
||||
len = (int)(sp - str);
|
||||
/*
|
||||
* Look it up and deal with it
|
||||
*/
|
||||
for (op = optlist; op <= &optlist[NUM_OPTS-1]; op++)
|
||||
if (EQSTR(str, op->o_name, len))
|
||||
{
|
||||
if (op->o_putfunc == put_bool) /* if option is a boolean */
|
||||
*(bool *)op->o_opt = TRUE; /* NOSTRICT */
|
||||
else /* string option */
|
||||
{
|
||||
/*
|
||||
* Skip to start of string value
|
||||
*/
|
||||
for (str = sp + 1; *str == '='; str++)
|
||||
continue;
|
||||
if (*str == '~')
|
||||
{
|
||||
strcpy((char *) op->o_opt, home); /* NOSTRICT */
|
||||
start = (char *) op->o_opt + strlen(home);/* NOSTRICT */
|
||||
while (*++str == '/')
|
||||
continue;
|
||||
}
|
||||
else
|
||||
start = (char *) op->o_opt; /* NOSTRICT */
|
||||
/*
|
||||
* Skip to end of string value
|
||||
*/
|
||||
for (sp = str + 1; *sp && *sp != ','; sp++)
|
||||
continue;
|
||||
/*
|
||||
* check for type of inventory
|
||||
*/
|
||||
if (op->o_putfunc == put_inv_t)
|
||||
{
|
||||
if (islower(*str))
|
||||
*str = (char) toupper(*str);
|
||||
for (i = inv_t_name; i <= &inv_t_name[INV_CLEAR]; i++)
|
||||
if (strncmp(str, *i, sp - str) == 0)
|
||||
{
|
||||
inv_type = (int)(i - inv_t_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
strucpy(start, str, (int)(sp - str));
|
||||
}
|
||||
break;
|
||||
}
|
||||
/*
|
||||
* check for "noname" for booleans
|
||||
*/
|
||||
else if (op->o_putfunc == put_bool
|
||||
&& EQSTR(str, "no", 2) && EQSTR(str + 2, op->o_name, len - 2))
|
||||
{
|
||||
*(bool *)op->o_opt = FALSE; /* NOSTRICT */
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* skip to start of next option name
|
||||
*/
|
||||
while (*sp && !isalpha(*sp))
|
||||
sp++;
|
||||
str = sp;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* strucpy:
|
||||
* Copy string using unctrl for things
|
||||
*/
|
||||
|
||||
void
|
||||
strucpy(char *s1, char *s2, int len)
|
||||
{
|
||||
if (len > MAXINP)
|
||||
len = MAXINP;
|
||||
while (len--)
|
||||
{
|
||||
if (isprint(*s2) || *s2 == ' ')
|
||||
*s1++ = *s2;
|
||||
s2++;
|
||||
}
|
||||
*s1 = '\0';
|
||||
}
|
||||
503
pack.c
Normal file
503
pack.c
Normal file
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
* Routines to deal with the pack
|
||||
*
|
||||
* @(#)pack.c 4.40 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <curses.h>
|
||||
#include <ctype.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* add_pack:
|
||||
* Pick up an object and add it to the pack. If the argument is
|
||||
* non-null use it as the linked_list pointer instead of gettting
|
||||
* it off the ground.
|
||||
*/
|
||||
|
||||
void
|
||||
add_pack(THING *obj, bool silent)
|
||||
{
|
||||
THING *op, *lp;
|
||||
bool from_floor;
|
||||
|
||||
from_floor = FALSE;
|
||||
if (obj == NULL)
|
||||
{
|
||||
if ((obj = find_obj(hero.y, hero.x)) == NULL)
|
||||
return;
|
||||
from_floor = TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check for and deal with scare monster scrolls
|
||||
*/
|
||||
if (obj->o_type == SCROLL && obj->o_which == S_SCARE)
|
||||
if (obj->o_flags & ISFOUND)
|
||||
{
|
||||
detach(lvl_obj, obj);
|
||||
mvaddch(hero.y, hero.x, floor_ch());
|
||||
chat(hero.y, hero.x) = (proom->r_flags & ISGONE) ? PASSAGE : FLOOR;
|
||||
discard(obj);
|
||||
msg("the scroll turns to dust as you pick it up");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pack == NULL)
|
||||
{
|
||||
pack = obj;
|
||||
obj->o_packch = pack_char();
|
||||
inpack++;
|
||||
}
|
||||
else
|
||||
{
|
||||
lp = NULL;
|
||||
for (op = pack; op != NULL; op = next(op))
|
||||
{
|
||||
if (op->o_type != obj->o_type)
|
||||
lp = op;
|
||||
else
|
||||
{
|
||||
while (op->o_type == obj->o_type && op->o_which != obj->o_which)
|
||||
{
|
||||
lp = op;
|
||||
if (next(op) == NULL)
|
||||
break;
|
||||
else
|
||||
op = next(op);
|
||||
}
|
||||
if (op->o_type == obj->o_type && op->o_which == obj->o_which)
|
||||
{
|
||||
if (ISMULT(op->o_type))
|
||||
{
|
||||
if (!pack_room(from_floor, obj))
|
||||
return;
|
||||
op->o_count++;
|
||||
dump_it:
|
||||
discard(obj);
|
||||
obj = op;
|
||||
lp = NULL;
|
||||
goto out;
|
||||
}
|
||||
else if (obj->o_group)
|
||||
{
|
||||
lp = op;
|
||||
while (op->o_type == obj->o_type
|
||||
&& op->o_which == obj->o_which
|
||||
&& op->o_group != obj->o_group)
|
||||
{
|
||||
lp = op;
|
||||
if (next(op) == NULL)
|
||||
break;
|
||||
else
|
||||
op = next(op);
|
||||
}
|
||||
if (op->o_type == obj->o_type
|
||||
&& op->o_which == obj->o_which
|
||||
&& op->o_group == obj->o_group)
|
||||
{
|
||||
op->o_count += obj->o_count;
|
||||
inpack--;
|
||||
if (!pack_room(from_floor, obj))
|
||||
return;
|
||||
goto dump_it;
|
||||
}
|
||||
}
|
||||
else
|
||||
lp = op;
|
||||
}
|
||||
out:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lp != NULL)
|
||||
{
|
||||
if (!pack_room(from_floor, obj))
|
||||
return;
|
||||
else
|
||||
{
|
||||
obj->o_packch = pack_char();
|
||||
next(obj) = next(lp);
|
||||
prev(obj) = lp;
|
||||
if (next(lp) != NULL)
|
||||
prev(next(lp)) = obj;
|
||||
next(lp) = obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obj->o_flags |= ISFOUND;
|
||||
|
||||
/*
|
||||
* If this was the object of something's desire, that monster will
|
||||
* get mad and run at the hero.
|
||||
*/
|
||||
for (op = mlist; op != NULL; op = next(op))
|
||||
if (op->t_dest == &obj->o_pos)
|
||||
op->t_dest = &hero;
|
||||
|
||||
if (obj->o_type == AMULET)
|
||||
amulet = TRUE;
|
||||
/*
|
||||
* Notify the user
|
||||
*/
|
||||
if (!silent)
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("you now have ");
|
||||
msg("%s (%c)", inv_name(obj, !terse), obj->o_packch);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* pack_room:
|
||||
* See if there's room in the pack. If not, print out an
|
||||
* appropriate message
|
||||
*/
|
||||
bool
|
||||
pack_room(bool from_floor, THING *obj)
|
||||
{
|
||||
if (++inpack > MAXPACK)
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("there's ");
|
||||
addmsg("no room");
|
||||
if (!terse)
|
||||
addmsg(" in your pack");
|
||||
endmsg();
|
||||
if (from_floor)
|
||||
move_msg(obj);
|
||||
inpack = MAXPACK;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (from_floor)
|
||||
{
|
||||
detach(lvl_obj, obj);
|
||||
mvaddch(hero.y, hero.x, floor_ch());
|
||||
chat(hero.y, hero.x) = (proom->r_flags & ISGONE) ? PASSAGE : FLOOR;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* leave_pack:
|
||||
* take an item out of the pack
|
||||
*/
|
||||
THING *
|
||||
leave_pack(THING *obj, bool newobj, bool all)
|
||||
{
|
||||
THING *nobj;
|
||||
|
||||
inpack--;
|
||||
nobj = obj;
|
||||
if (obj->o_count > 1 && !all)
|
||||
{
|
||||
last_pick = obj;
|
||||
obj->o_count--;
|
||||
if (obj->o_group)
|
||||
inpack++;
|
||||
if (newobj)
|
||||
{
|
||||
nobj = new_item();
|
||||
*nobj = *obj;
|
||||
next(nobj) = NULL;
|
||||
prev(nobj) = NULL;
|
||||
nobj->o_count = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
last_pick = NULL;
|
||||
pack_used[obj->o_packch - 'a'] = FALSE;
|
||||
detach(pack, obj);
|
||||
}
|
||||
return nobj;
|
||||
}
|
||||
|
||||
/*
|
||||
* pack_char:
|
||||
* Return the next unused pack character.
|
||||
*/
|
||||
char
|
||||
pack_char()
|
||||
{
|
||||
bool *bp;
|
||||
|
||||
for (bp = pack_used; *bp; bp++)
|
||||
continue;
|
||||
*bp = TRUE;
|
||||
return (char)((int)(bp - pack_used) + 'a');
|
||||
}
|
||||
|
||||
/*
|
||||
* inventory:
|
||||
* List what is in the pack. Return TRUE if there is something of
|
||||
* the given type.
|
||||
*/
|
||||
bool
|
||||
inventory(THING *list, int type)
|
||||
{
|
||||
static char inv_temp[MAXSTR];
|
||||
|
||||
n_objs = 0;
|
||||
for (; list != NULL; list = next(list))
|
||||
{
|
||||
if (type && type != list->o_type && !(type == CALLABLE &&
|
||||
list->o_type != FOOD && list->o_type != AMULET) &&
|
||||
!(type == R_OR_S && (list->o_type == RING || list->o_type == STICK)))
|
||||
continue;
|
||||
n_objs++;
|
||||
#ifdef MASTER
|
||||
if (!list->o_packch)
|
||||
strcpy(inv_temp, "%s");
|
||||
else
|
||||
#endif
|
||||
sprintf(inv_temp, "%c) %%s", list->o_packch);
|
||||
msg_esc = TRUE;
|
||||
if (add_line(inv_temp, inv_name(list, FALSE)) == ESCAPE)
|
||||
{
|
||||
msg_esc = FALSE;
|
||||
msg("");
|
||||
return TRUE;
|
||||
}
|
||||
msg_esc = FALSE;
|
||||
}
|
||||
if (n_objs == 0)
|
||||
{
|
||||
if (terse)
|
||||
msg(type == 0 ? "empty handed" :
|
||||
"nothing appropriate");
|
||||
else
|
||||
msg(type == 0 ? "you are empty handed" :
|
||||
"you don't have anything appropriate");
|
||||
return FALSE;
|
||||
}
|
||||
end_line();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* pick_up:
|
||||
* Add something to characters pack.
|
||||
*/
|
||||
|
||||
void
|
||||
pick_up(char ch)
|
||||
{
|
||||
THING *obj;
|
||||
|
||||
if (on(player, ISLEVIT))
|
||||
return;
|
||||
|
||||
obj = find_obj(hero.y, hero.x);
|
||||
if (move_on)
|
||||
move_msg(obj);
|
||||
else
|
||||
switch (ch)
|
||||
{
|
||||
case GOLD:
|
||||
if (obj == NULL)
|
||||
return;
|
||||
money(obj->o_goldval);
|
||||
detach(lvl_obj, obj);
|
||||
discard(obj);
|
||||
proom->r_goldval = 0;
|
||||
break;
|
||||
default:
|
||||
#ifdef MASTER
|
||||
debug("Where did you pick a '%s' up???", unctrl(ch));
|
||||
#endif
|
||||
case ARMOR:
|
||||
case POTION:
|
||||
case FOOD:
|
||||
case WEAPON:
|
||||
case SCROLL:
|
||||
case AMULET:
|
||||
case RING:
|
||||
case STICK:
|
||||
add_pack((THING *) NULL, FALSE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* move_msg:
|
||||
* Print out the message if you are just moving onto an object
|
||||
*/
|
||||
|
||||
void
|
||||
move_msg(THING *obj)
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("you ");
|
||||
msg("moved onto %s", inv_name(obj, TRUE));
|
||||
}
|
||||
|
||||
/*
|
||||
* picky_inven:
|
||||
* Allow player to inventory a single item
|
||||
*/
|
||||
|
||||
void
|
||||
picky_inven()
|
||||
{
|
||||
THING *obj;
|
||||
char mch;
|
||||
|
||||
if (pack == NULL)
|
||||
msg("you aren't carrying anything");
|
||||
else if (next(pack) == NULL)
|
||||
msg("a) %s", inv_name(pack, FALSE));
|
||||
else
|
||||
{
|
||||
msg(terse ? "item: " : "which item do you wish to inventory: ");
|
||||
mpos = 0;
|
||||
if ((mch = readchar()) == ESCAPE)
|
||||
{
|
||||
msg("");
|
||||
return;
|
||||
}
|
||||
for (obj = pack; obj != NULL; obj = next(obj))
|
||||
if (mch == obj->o_packch)
|
||||
{
|
||||
msg("%c) %s", mch, inv_name(obj, FALSE));
|
||||
return;
|
||||
}
|
||||
msg("'%s' not in pack", unctrl(mch));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* get_item:
|
||||
* Pick something out of a pack for a purpose
|
||||
*/
|
||||
THING *
|
||||
get_item(char *purpose, int type)
|
||||
{
|
||||
THING *obj;
|
||||
char ch;
|
||||
|
||||
if (pack == NULL)
|
||||
msg("you aren't carrying anything");
|
||||
else if (again)
|
||||
if (last_pick)
|
||||
return last_pick;
|
||||
else
|
||||
msg("you ran out");
|
||||
else
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("which object do you want to ");
|
||||
addmsg(purpose);
|
||||
if (terse)
|
||||
addmsg(" what");
|
||||
msg("? (* for list): ");
|
||||
ch = readchar();
|
||||
mpos = 0;
|
||||
/*
|
||||
* Give the poor player a chance to abort the command
|
||||
*/
|
||||
if (ch == ESCAPE)
|
||||
{
|
||||
reset_last();
|
||||
after = FALSE;
|
||||
msg("");
|
||||
return NULL;
|
||||
}
|
||||
n_objs = 1; /* normal case: person types one char */
|
||||
if (ch == '*')
|
||||
{
|
||||
mpos = 0;
|
||||
if (inventory(pack, type) == 0)
|
||||
{
|
||||
after = FALSE;
|
||||
return NULL;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (obj = pack; obj != NULL; obj = next(obj))
|
||||
if (obj->o_packch == ch)
|
||||
break;
|
||||
if (obj == NULL)
|
||||
{
|
||||
msg("'%s' is not a valid item",unctrl(ch));
|
||||
continue;
|
||||
}
|
||||
else
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* money:
|
||||
* Add or subtract gold from the pack
|
||||
*/
|
||||
|
||||
void
|
||||
money(int value)
|
||||
{
|
||||
purse += value;
|
||||
mvaddch(hero.y, hero.x, floor_ch());
|
||||
chat(hero.y, hero.x) = (proom->r_flags & ISGONE) ? PASSAGE : FLOOR;
|
||||
if (value > 0)
|
||||
{
|
||||
if (!terse)
|
||||
addmsg("you found ");
|
||||
msg("%d gold pieces", value);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* floor_ch:
|
||||
* Return the appropriate floor character for her room
|
||||
*/
|
||||
char
|
||||
floor_ch()
|
||||
{
|
||||
if (proom->r_flags & ISGONE)
|
||||
return PASSAGE;
|
||||
return (show_floor() ? FLOOR : ' ');
|
||||
}
|
||||
|
||||
/*
|
||||
* floor_at:
|
||||
* Return the character at hero's position, taking see_floor
|
||||
* into account
|
||||
*/
|
||||
char
|
||||
floor_at()
|
||||
{
|
||||
char ch;
|
||||
|
||||
ch = chat(hero.y, hero.x);
|
||||
if (ch == FLOOR)
|
||||
ch = floor_ch();
|
||||
return ch;
|
||||
}
|
||||
|
||||
/*
|
||||
* reset_last:
|
||||
* Reset the last command when the current one is aborted
|
||||
*/
|
||||
|
||||
void
|
||||
reset_last()
|
||||
{
|
||||
last_comm = l_last_comm;
|
||||
last_dir = l_last_dir;
|
||||
last_pick = l_last_pick;
|
||||
}
|
||||
424
passages.c
Normal file
424
passages.c
Normal file
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* Draw the connecting passages
|
||||
*
|
||||
* @(#)passages.c 4.22 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* do_passages:
|
||||
* Draw all the passages on a level.
|
||||
*/
|
||||
|
||||
void
|
||||
do_passages()
|
||||
{
|
||||
struct rdes *r1, *r2 = NULL;
|
||||
int i, j;
|
||||
int roomcount;
|
||||
static struct rdes
|
||||
{
|
||||
bool conn[MAXROOMS]; /* possible to connect to room i? */
|
||||
bool isconn[MAXROOMS]; /* connection been made to room i? */
|
||||
bool ingraph; /* this room in graph already? */
|
||||
} rdes[MAXROOMS] = {
|
||||
{ { 0, 1, 0, 1, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
{ { 1, 0, 1, 0, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
{ { 0, 1, 0, 0, 0, 1, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
{ { 1, 0, 0, 0, 1, 0, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
{ { 0, 1, 0, 1, 0, 1, 0, 1, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
{ { 0, 0, 1, 0, 1, 0, 0, 0, 1 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
{ { 0, 0, 0, 1, 0, 0, 0, 1, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
{ { 0, 0, 0, 0, 1, 0, 1, 0, 1 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
{ { 0, 0, 0, 0, 0, 1, 0, 1, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0 },
|
||||
};
|
||||
|
||||
/*
|
||||
* reinitialize room graph description
|
||||
*/
|
||||
for (r1 = rdes; r1 <= &rdes[MAXROOMS-1]; r1++)
|
||||
{
|
||||
for (j = 0; j < MAXROOMS; j++)
|
||||
r1->isconn[j] = FALSE;
|
||||
r1->ingraph = FALSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* starting with one room, connect it to a random adjacent room and
|
||||
* then pick a new room to start with.
|
||||
*/
|
||||
roomcount = 1;
|
||||
r1 = &rdes[rnd(MAXROOMS)];
|
||||
r1->ingraph = TRUE;
|
||||
do
|
||||
{
|
||||
/*
|
||||
* find a room to connect with
|
||||
*/
|
||||
j = 0;
|
||||
for (i = 0; i < MAXROOMS; i++)
|
||||
if (r1->conn[i] && !rdes[i].ingraph && rnd(++j) == 0)
|
||||
r2 = &rdes[i];
|
||||
/*
|
||||
* if no adjacent rooms are outside the graph, pick a new room
|
||||
* to look from
|
||||
*/
|
||||
if (j == 0)
|
||||
{
|
||||
do
|
||||
r1 = &rdes[rnd(MAXROOMS)];
|
||||
until (r1->ingraph);
|
||||
}
|
||||
/*
|
||||
* otherwise, connect new room to the graph, and draw a tunnel
|
||||
* to it
|
||||
*/
|
||||
else
|
||||
{
|
||||
r2->ingraph = TRUE;
|
||||
i = (int)(r1 - rdes);
|
||||
j = (int)(r2 - rdes);
|
||||
conn(i, j);
|
||||
r1->isconn[j] = TRUE;
|
||||
r2->isconn[i] = TRUE;
|
||||
roomcount++;
|
||||
}
|
||||
} while (roomcount < MAXROOMS);
|
||||
|
||||
/*
|
||||
* attempt to add passages to the graph a random number of times so
|
||||
* that there isn't always just one unique passage through it.
|
||||
*/
|
||||
for (roomcount = rnd(5); roomcount > 0; roomcount--)
|
||||
{
|
||||
r1 = &rdes[rnd(MAXROOMS)]; /* a random room to look from */
|
||||
/*
|
||||
* find an adjacent room not already connected
|
||||
*/
|
||||
j = 0;
|
||||
for (i = 0; i < MAXROOMS; i++)
|
||||
if (r1->conn[i] && !r1->isconn[i] && rnd(++j) == 0)
|
||||
r2 = &rdes[i];
|
||||
/*
|
||||
* if there is one, connect it and look for the next added
|
||||
* passage
|
||||
*/
|
||||
if (j != 0)
|
||||
{
|
||||
i = (int)(r1 - rdes);
|
||||
j = (int)(r2 - rdes);
|
||||
conn(i, j);
|
||||
r1->isconn[j] = TRUE;
|
||||
r2->isconn[i] = TRUE;
|
||||
}
|
||||
}
|
||||
passnum();
|
||||
}
|
||||
|
||||
/*
|
||||
* conn:
|
||||
* Draw a corridor from a room in a certain direction.
|
||||
*/
|
||||
|
||||
void
|
||||
conn(int r1, int r2)
|
||||
{
|
||||
struct room *rpf, *rpt = NULL;
|
||||
int rmt;
|
||||
int distance = 0, turn_spot, turn_distance = 0;
|
||||
int rm;
|
||||
char direc;
|
||||
static coord del, curr, turn_delta, spos, epos;
|
||||
|
||||
if (r1 < r2)
|
||||
{
|
||||
rm = r1;
|
||||
if (r1 + 1 == r2)
|
||||
direc = 'r';
|
||||
else
|
||||
direc = 'd';
|
||||
}
|
||||
else
|
||||
{
|
||||
rm = r2;
|
||||
if (r2 + 1 == r1)
|
||||
direc = 'r';
|
||||
else
|
||||
direc = 'd';
|
||||
}
|
||||
rpf = &rooms[rm];
|
||||
/*
|
||||
* Set up the movement variables, in two cases:
|
||||
* first drawing one down.
|
||||
*/
|
||||
if (direc == 'd')
|
||||
{
|
||||
rmt = rm + 3; /* room # of dest */
|
||||
rpt = &rooms[rmt]; /* room pointer of dest */
|
||||
del.x = 0; /* direction of move */
|
||||
del.y = 1;
|
||||
spos.x = rpf->r_pos.x; /* start of move */
|
||||
spos.y = rpf->r_pos.y;
|
||||
epos.x = rpt->r_pos.x; /* end of move */
|
||||
epos.y = rpt->r_pos.y;
|
||||
if (!(rpf->r_flags & ISGONE)) /* if not gone pick door pos */
|
||||
do
|
||||
{
|
||||
spos.x = rpf->r_pos.x + rnd(rpf->r_max.x - 2) + 1;
|
||||
spos.y = rpf->r_pos.y + rpf->r_max.y - 1;
|
||||
} while ((rpf->r_flags&ISMAZE) && !(flat(spos.y, spos.x)&F_PASS));
|
||||
if (!(rpt->r_flags & ISGONE))
|
||||
do
|
||||
{
|
||||
epos.x = rpt->r_pos.x + rnd(rpt->r_max.x - 2) + 1;
|
||||
} while ((rpt->r_flags&ISMAZE) && !(flat(epos.y, epos.x)&F_PASS));
|
||||
distance = abs(spos.y - epos.y) - 1; /* distance to move */
|
||||
turn_delta.y = 0; /* direction to turn */
|
||||
turn_delta.x = (spos.x < epos.x ? 1 : -1);
|
||||
turn_distance = abs(spos.x - epos.x); /* how far to turn */
|
||||
}
|
||||
else if (direc == 'r') /* setup for moving right */
|
||||
{
|
||||
rmt = rm + 1;
|
||||
rpt = &rooms[rmt];
|
||||
del.x = 1;
|
||||
del.y = 0;
|
||||
spos.x = rpf->r_pos.x;
|
||||
spos.y = rpf->r_pos.y;
|
||||
epos.x = rpt->r_pos.x;
|
||||
epos.y = rpt->r_pos.y;
|
||||
if (!(rpf->r_flags & ISGONE))
|
||||
do
|
||||
{
|
||||
spos.x = rpf->r_pos.x + rpf->r_max.x - 1;
|
||||
spos.y = rpf->r_pos.y + rnd(rpf->r_max.y - 2) + 1;
|
||||
} while ((rpf->r_flags&ISMAZE) && !(flat(spos.y, spos.x)&F_PASS));
|
||||
if (!(rpt->r_flags & ISGONE))
|
||||
do
|
||||
{
|
||||
epos.y = rpt->r_pos.y + rnd(rpt->r_max.y - 2) + 1;
|
||||
} while ((rpt->r_flags&ISMAZE) && !(flat(epos.y, epos.x)&F_PASS));
|
||||
distance = abs(spos.x - epos.x) - 1;
|
||||
turn_delta.y = (spos.y < epos.y ? 1 : -1);
|
||||
turn_delta.x = 0;
|
||||
turn_distance = abs(spos.y - epos.y);
|
||||
}
|
||||
#ifdef MASTER
|
||||
else
|
||||
debug("error in connection tables");
|
||||
#endif
|
||||
|
||||
turn_spot = rnd(distance - 1) + 1; /* where turn starts */
|
||||
|
||||
/*
|
||||
* Draw in the doors on either side of the passage or just put #'s
|
||||
* if the rooms are gone.
|
||||
*/
|
||||
if (!(rpf->r_flags & ISGONE))
|
||||
door(rpf, &spos);
|
||||
else
|
||||
putpass(&spos);
|
||||
if (!(rpt->r_flags & ISGONE))
|
||||
door(rpt, &epos);
|
||||
else
|
||||
putpass(&epos);
|
||||
/*
|
||||
* Get ready to move...
|
||||
*/
|
||||
curr.x = spos.x;
|
||||
curr.y = spos.y;
|
||||
while (distance > 0)
|
||||
{
|
||||
/*
|
||||
* Move to new position
|
||||
*/
|
||||
curr.x += del.x;
|
||||
curr.y += del.y;
|
||||
/*
|
||||
* Check if we are at the turn place, if so do the turn
|
||||
*/
|
||||
if (distance == turn_spot)
|
||||
while (turn_distance--)
|
||||
{
|
||||
putpass(&curr);
|
||||
curr.x += turn_delta.x;
|
||||
curr.y += turn_delta.y;
|
||||
}
|
||||
/*
|
||||
* Continue digging along
|
||||
*/
|
||||
putpass(&curr);
|
||||
distance--;
|
||||
}
|
||||
curr.x += del.x;
|
||||
curr.y += del.y;
|
||||
if (!ce(curr, epos))
|
||||
msg("warning, connectivity problem on this level");
|
||||
}
|
||||
|
||||
/*
|
||||
* putpass:
|
||||
* add a passage character or secret passage here
|
||||
*/
|
||||
|
||||
void
|
||||
putpass(coord *cp)
|
||||
{
|
||||
PLACE *pp;
|
||||
|
||||
pp = INDEX(cp->y, cp->x);
|
||||
pp->p_flags |= F_PASS;
|
||||
if (rnd(10) + 1 < level && rnd(40) == 0)
|
||||
pp->p_flags &= ~F_REAL;
|
||||
else
|
||||
pp->p_ch = PASSAGE;
|
||||
}
|
||||
|
||||
/*
|
||||
* door:
|
||||
* Add a door or possibly a secret door. Also enters the door in
|
||||
* the exits array of the room.
|
||||
*/
|
||||
|
||||
void
|
||||
door(struct room *rm, coord *cp)
|
||||
{
|
||||
PLACE *pp;
|
||||
|
||||
rm->r_exit[rm->r_nexits++] = *cp;
|
||||
|
||||
if (rm->r_flags & ISMAZE)
|
||||
return;
|
||||
|
||||
pp = INDEX(cp->y, cp->x);
|
||||
if (rnd(10) + 1 < level && rnd(5) == 0)
|
||||
{
|
||||
if (cp->y == rm->r_pos.y || cp->y == rm->r_pos.y + rm->r_max.y - 1)
|
||||
pp->p_ch = '-';
|
||||
else
|
||||
pp->p_ch = '|';
|
||||
pp->p_flags &= ~F_REAL;
|
||||
}
|
||||
else
|
||||
pp->p_ch = DOOR;
|
||||
}
|
||||
|
||||
#ifdef MASTER
|
||||
/*
|
||||
* add_pass:
|
||||
* Add the passages to the current window (wizard command)
|
||||
*/
|
||||
|
||||
void
|
||||
add_pass()
|
||||
{
|
||||
PLACE *pp;
|
||||
int y, x;
|
||||
char ch;
|
||||
|
||||
for (y = 1; y < NUMLINES - 1; y++)
|
||||
for (x = 0; x < NUMCOLS; x++)
|
||||
{
|
||||
pp = INDEX(y, x);
|
||||
if ((pp->p_flags & F_PASS) || pp->p_ch == DOOR ||
|
||||
(!(pp->p_flags&F_REAL) && (pp->p_ch == '|' || pp->p_ch == '-')))
|
||||
{
|
||||
ch = pp->p_ch;
|
||||
if (pp->p_flags & F_PASS)
|
||||
ch = PASSAGE;
|
||||
pp->p_flags |= F_SEEN;
|
||||
move(y, x);
|
||||
if (pp->p_monst != NULL)
|
||||
pp->p_monst->t_oldch = pp->p_ch;
|
||||
else if (pp->p_flags & F_REAL)
|
||||
addch(ch);
|
||||
else
|
||||
{
|
||||
standout();
|
||||
addch((pp->p_flags & F_PASS) ? PASSAGE : DOOR);
|
||||
standend();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* passnum:
|
||||
* Assign a number to each passageway
|
||||
*/
|
||||
static int pnum;
|
||||
static bool newpnum;
|
||||
|
||||
|
||||
void
|
||||
passnum()
|
||||
{
|
||||
struct room *rp;
|
||||
int i;
|
||||
|
||||
pnum = 0;
|
||||
newpnum = FALSE;
|
||||
for (rp = passages; rp < &passages[MAXPASS]; rp++)
|
||||
rp->r_nexits = 0;
|
||||
for (rp = rooms; rp < &rooms[MAXROOMS]; rp++)
|
||||
for (i = 0; i < rp->r_nexits; i++)
|
||||
{
|
||||
newpnum++;
|
||||
numpass(rp->r_exit[i].y, rp->r_exit[i].x);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* numpass:
|
||||
* Number a passageway square and its brethren
|
||||
*/
|
||||
|
||||
void
|
||||
numpass(int y, int x)
|
||||
{
|
||||
char *fp;
|
||||
struct room *rp;
|
||||
char ch;
|
||||
|
||||
if (x >= NUMCOLS || x < 0 || y >= NUMLINES || y <= 0)
|
||||
return;
|
||||
fp = &flat(y, x);
|
||||
if (*fp & F_PNUM)
|
||||
return;
|
||||
if (newpnum)
|
||||
{
|
||||
pnum++;
|
||||
newpnum = FALSE;
|
||||
}
|
||||
/*
|
||||
* check to see if it is a door or secret door, i.e., a new exit,
|
||||
* or a numerable type of place
|
||||
*/
|
||||
if ((ch = chat(y, x)) == DOOR ||
|
||||
(!(*fp & F_REAL) && (ch == '|' || ch == '-')))
|
||||
{
|
||||
rp = &passages[pnum];
|
||||
rp->r_exit[rp->r_nexits].y = y;
|
||||
rp->r_exit[rp->r_nexits++].x = x;
|
||||
}
|
||||
else if (!(*fp & F_PASS))
|
||||
return;
|
||||
*fp |= pnum;
|
||||
/*
|
||||
* recurse on the surrounding places
|
||||
*/
|
||||
numpass(y + 1, x);
|
||||
numpass(y - 1, x);
|
||||
numpass(y, x + 1);
|
||||
numpass(y, x - 1);
|
||||
}
|
||||
375
potions.c
Normal file
375
potions.c
Normal file
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* Function(s) for dealing with potions
|
||||
*
|
||||
* @(#)potions.c 4.46 (Berkeley) 06/07/83
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include <ctype.h>
|
||||
#include "rogue.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int pa_flags;
|
||||
void (*pa_daemon)();
|
||||
int pa_time;
|
||||
char *pa_high, *pa_straight;
|
||||
} PACT;
|
||||
|
||||
static PACT p_actions[] =
|
||||
{
|
||||
{ ISHUH, unconfuse, HUHDURATION, /* P_CONFUSE */
|
||||
"what a tripy feeling!",
|
||||
"wait, what's going on here. Huh? What? Who?" },
|
||||
{ ISHALU, come_down, SEEDURATION, /* P_LSD */
|
||||
"Oh, wow! Everything seems so cosmic!",
|
||||
"Oh, wow! Everything seems so cosmic!" },
|
||||
{ 0, NULL, 0 }, /* P_POISON */
|
||||
{ 0, NULL, 0 }, /* P_STRENGTH */
|
||||
{ CANSEE, unsee, SEEDURATION, /* P_SEEINVIS */
|
||||
prbuf,
|
||||
prbuf },
|
||||
{ 0, NULL, 0 }, /* P_HEALING */
|
||||
{ 0, NULL, 0 }, /* P_MFIND */
|
||||
{ 0, NULL, 0 }, /* P_TFIND */
|
||||
{ 0, NULL, 0 }, /* P_RAISE */
|
||||
{ 0, NULL, 0 }, /* P_XHEAL */
|
||||
{ 0, NULL, 0 }, /* P_HASTE */
|
||||
{ 0, NULL, 0 }, /* P_RESTORE */
|
||||
{ ISBLIND, sight, SEEDURATION, /* P_BLIND */
|
||||
"oh, bummer! Everything is dark! Help!",
|
||||
"a cloak of darkness falls around you" },
|
||||
{ ISLEVIT, land, HEALTIME, /* P_LEVIT */
|
||||
"oh, wow! You're floating in the air!",
|
||||
"you start to float in the air" }
|
||||
};
|
||||
|
||||
/*
|
||||
* quaff:
|
||||
* Quaff a potion from the pack
|
||||
*/
|
||||
|
||||
void
|
||||
quaff()
|
||||
{
|
||||
THING *obj, *tp, *mp;
|
||||
bool discardit = FALSE;
|
||||
bool show, trip;
|
||||
|
||||
obj = get_item("quaff", POTION);
|
||||
/*
|
||||
* Make certain that it is somethings that we want to drink
|
||||
*/
|
||||
if (obj == NULL)
|
||||
return;
|
||||
if (obj->o_type != POTION)
|
||||
{
|
||||
if (!terse)
|
||||
msg("yuk! Why would you want to drink that?");
|
||||
else
|
||||
msg("that's undrinkable");
|
||||
return;
|
||||
}
|
||||
if (obj == cur_weapon)
|
||||
cur_weapon = NULL;
|
||||
|
||||
/*
|
||||
* Calculate the effect it has on the poor guy.
|
||||
*/
|
||||
trip = on(player, ISHALU);
|
||||
discardit = (bool)(obj->o_count == 1);
|
||||
leave_pack(obj, FALSE, FALSE);
|
||||
switch (obj->o_which)
|
||||
{
|
||||
case P_CONFUSE:
|
||||
do_pot(P_CONFUSE, !trip);
|
||||
when P_POISON:
|
||||
pot_info[P_POISON].oi_know = TRUE;
|
||||
if (ISWEARING(R_SUSTSTR))
|
||||
msg("you feel momentarily sick");
|
||||
else
|
||||
{
|
||||
chg_str(-(rnd(3) + 1));
|
||||
msg("you feel very sick now");
|
||||
come_down();
|
||||
}
|
||||
when P_HEALING:
|
||||
pot_info[P_HEALING].oi_know = TRUE;
|
||||
if ((pstats.s_hpt += roll(pstats.s_lvl, 4)) > max_hp)
|
||||
pstats.s_hpt = ++max_hp;
|
||||
sight();
|
||||
msg("you begin to feel better");
|
||||
when P_STRENGTH:
|
||||
pot_info[P_STRENGTH].oi_know = TRUE;
|
||||
chg_str(1);
|
||||
msg("you feel stronger, now. What bulging muscles!");
|
||||
when P_MFIND:
|
||||
player.t_flags |= SEEMONST;
|
||||
fuse((void(*)())turn_see, TRUE, HUHDURATION, AFTER);
|
||||
if (!turn_see(FALSE))
|
||||
msg("you have a %s feeling for a moment, then it passes",
|
||||
choose_str("normal", "strange"));
|
||||
when P_TFIND:
|
||||
/*
|
||||
* Potion of magic detection. Show the potions and scrolls
|
||||
*/
|
||||
show = FALSE;
|
||||
if (lvl_obj != NULL)
|
||||
{
|
||||
wclear(hw);
|
||||
for (tp = lvl_obj; tp != NULL; tp = next(tp))
|
||||
{
|
||||
if (is_magic(tp))
|
||||
{
|
||||
show = TRUE;
|
||||
wmove(hw, tp->o_pos.y, tp->o_pos.x);
|
||||
waddch(hw, MAGIC);
|
||||
pot_info[P_TFIND].oi_know = TRUE;
|
||||
}
|
||||
}
|
||||
for (mp = mlist; mp != NULL; mp = next(mp))
|
||||
{
|
||||
for (tp = mp->t_pack; tp != NULL; tp = next(tp))
|
||||
{
|
||||
if (is_magic(tp))
|
||||
{
|
||||
show = TRUE;
|
||||
wmove(hw, mp->t_pos.y, mp->t_pos.x);
|
||||
waddch(hw, MAGIC);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (show)
|
||||
{
|
||||
pot_info[P_TFIND].oi_know = TRUE;
|
||||
show_win("You sense the presence of magic on this level.--More--");
|
||||
}
|
||||
else
|
||||
msg("you have a %s feeling for a moment, then it passes",
|
||||
choose_str("normal", "strange"));
|
||||
when P_LSD:
|
||||
if (!trip)
|
||||
{
|
||||
if (on(player, SEEMONST))
|
||||
turn_see(FALSE);
|
||||
start_daemon(visuals, 0, BEFORE);
|
||||
seenstairs = seen_stairs();
|
||||
}
|
||||
do_pot(P_LSD, TRUE);
|
||||
when P_SEEINVIS:
|
||||
sprintf(prbuf, "this potion tastes like %s juice", fruit);
|
||||
show = on(player, CANSEE);
|
||||
do_pot(P_SEEINVIS, FALSE);
|
||||
if (!show)
|
||||
invis_on();
|
||||
sight();
|
||||
when P_RAISE:
|
||||
pot_info[P_RAISE].oi_know = TRUE;
|
||||
msg("you suddenly feel much more skillful");
|
||||
raise_level();
|
||||
when P_XHEAL:
|
||||
pot_info[P_XHEAL].oi_know = TRUE;
|
||||
if ((pstats.s_hpt += roll(pstats.s_lvl, 8)) > max_hp)
|
||||
{
|
||||
if (pstats.s_hpt > max_hp + pstats.s_lvl + 1)
|
||||
++max_hp;
|
||||
pstats.s_hpt = ++max_hp;
|
||||
}
|
||||
sight();
|
||||
come_down();
|
||||
msg("you begin to feel much better");
|
||||
when P_HASTE:
|
||||
pot_info[P_HASTE].oi_know = TRUE;
|
||||
after = FALSE;
|
||||
if (add_haste(TRUE))
|
||||
msg("you feel yourself moving much faster");
|
||||
when P_RESTORE:
|
||||
if (ISRING(LEFT, R_ADDSTR))
|
||||
add_str(&pstats.s_str, -cur_ring[LEFT]->o_arm);
|
||||
if (ISRING(RIGHT, R_ADDSTR))
|
||||
add_str(&pstats.s_str, -cur_ring[RIGHT]->o_arm);
|
||||
if (pstats.s_str < max_stats.s_str)
|
||||
pstats.s_str = max_stats.s_str;
|
||||
if (ISRING(LEFT, R_ADDSTR))
|
||||
add_str(&pstats.s_str, cur_ring[LEFT]->o_arm);
|
||||
if (ISRING(RIGHT, R_ADDSTR))
|
||||
add_str(&pstats.s_str, cur_ring[RIGHT]->o_arm);
|
||||
msg("hey, this tastes great. It make you feel warm all over");
|
||||
when P_BLIND:
|
||||
do_pot(P_BLIND, TRUE);
|
||||
when P_LEVIT:
|
||||
do_pot(P_LEVIT, TRUE);
|
||||
#ifdef MASTER
|
||||
otherwise:
|
||||
msg("what an odd tasting potion!");
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
status();
|
||||
/*
|
||||
* Throw the item away
|
||||
*/
|
||||
|
||||
call_it(&pot_info[obj->o_which]);
|
||||
|
||||
if (discardit)
|
||||
discard(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* is_magic:
|
||||
* Returns true if an object radiates magic
|
||||
*/
|
||||
bool
|
||||
is_magic(THING *obj)
|
||||
{
|
||||
switch (obj->o_type)
|
||||
{
|
||||
case ARMOR:
|
||||
return (bool)((obj->o_flags&ISPROT) || obj->o_arm != a_class[obj->o_which]);
|
||||
case WEAPON:
|
||||
return (bool)(obj->o_hplus != 0 || obj->o_dplus != 0);
|
||||
case POTION:
|
||||
case SCROLL:
|
||||
case STICK:
|
||||
case RING:
|
||||
case AMULET:
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* invis_on:
|
||||
* Turn on the ability to see invisible
|
||||
*/
|
||||
|
||||
void
|
||||
invis_on()
|
||||
{
|
||||
THING *mp;
|
||||
|
||||
player.t_flags |= CANSEE;
|
||||
for (mp = mlist; mp != NULL; mp = next(mp))
|
||||
if (on(*mp, ISINVIS) && see_monst(mp) && !on(player, ISHALU))
|
||||
mvaddch(mp->t_pos.y, mp->t_pos.x, mp->t_disguise);
|
||||
}
|
||||
|
||||
/*
|
||||
* turn_see:
|
||||
* Put on or off seeing monsters on this level
|
||||
*/
|
||||
bool
|
||||
turn_see(bool turn_off)
|
||||
{
|
||||
THING *mp;
|
||||
bool can_see, add_new;
|
||||
|
||||
add_new = FALSE;
|
||||
for (mp = mlist; mp != NULL; mp = next(mp))
|
||||
{
|
||||
move(mp->t_pos.y, mp->t_pos.x);
|
||||
can_see = see_monst(mp);
|
||||
if (turn_off)
|
||||
{
|
||||
if (!can_see)
|
||||
addch(mp->t_oldch);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!can_see)
|
||||
standout();
|
||||
if (!on(player, ISHALU))
|
||||
addch(mp->t_type);
|
||||
else
|
||||
addch(rnd(26) + 'A');
|
||||
if (!can_see)
|
||||
{
|
||||
standend();
|
||||
add_new++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (turn_off)
|
||||
player.t_flags &= ~SEEMONST;
|
||||
else
|
||||
player.t_flags |= SEEMONST;
|
||||
return add_new;
|
||||
}
|
||||
|
||||
/*
|
||||
* seen_stairs:
|
||||
* Return TRUE if the player has seen the stairs
|
||||
*/
|
||||
bool
|
||||
seen_stairs()
|
||||
{
|
||||
THING *tp;
|
||||
|
||||
move(stairs.y, stairs.x);
|
||||
if (inch() == STAIRS) /* it's on the map */
|
||||
return TRUE;
|
||||
if (ce(hero, stairs)) /* It's under him */
|
||||
return TRUE;
|
||||
|
||||
/*
|
||||
* if a monster is on the stairs, this gets hairy
|
||||
*/
|
||||
if ((tp = moat(stairs.y, stairs.x)) != NULL)
|
||||
{
|
||||
if (see_monst(tp) && on(*tp, ISRUN)) /* if it's visible and awake */
|
||||
return TRUE; /* it must have moved there */
|
||||
|
||||
if (on(player, SEEMONST) /* if she can detect monster */
|
||||
&& tp->t_oldch == STAIRS) /* and there once were stairs */
|
||||
return TRUE; /* it must have moved there */
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/*
|
||||
* raise_level:
|
||||
* The guy just magically went up a level.
|
||||
*/
|
||||
|
||||
void
|
||||
raise_level()
|
||||
{
|
||||
pstats.s_exp = e_levels[pstats.s_lvl-1] + 1L;
|
||||
check_level();
|
||||
}
|
||||
|
||||
/*
|
||||
* do_pot:
|
||||
* Do a potion with standard setup. This means it uses a fuse and
|
||||
* turns on a flag
|
||||
*/
|
||||
|
||||
void
|
||||
do_pot(int type, bool knowit)
|
||||
{
|
||||
PACT *pp;
|
||||
int t;
|
||||
|
||||
pp = &p_actions[type];
|
||||
if (!pot_info[type].oi_know)
|
||||
pot_info[type].oi_know = knowit;
|
||||
t = spread(pp->pa_time);
|
||||
if (!on(player, pp->pa_flags))
|
||||
{
|
||||
player.t_flags |= pp->pa_flags;
|
||||
fuse(pp->pa_daemon, 0, t, AFTER);
|
||||
look(FALSE);
|
||||
}
|
||||
else
|
||||
lengthen(pp->pa_daemon, t);
|
||||
msg(choose_str(pp->pa_high, pp->pa_straight));
|
||||
}
|
||||
204
rings.c
Normal file
204
rings.c
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Routines dealing specifically with rings
|
||||
*
|
||||
* @(#)rings.c 4.19 (Berkeley) 05/29/83
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
|
||||
/*
|
||||
* ring_on:
|
||||
* Put a ring on a hand
|
||||
*/
|
||||
|
||||
void
|
||||
ring_on()
|
||||
{
|
||||
THING *obj;
|
||||
int ring;
|
||||
|
||||
obj = get_item("put on", RING);
|
||||
/*
|
||||
* Make certain that it is somethings that we want to wear
|
||||
*/
|
||||
if (obj == NULL)
|
||||
return;
|
||||
if (obj->o_type != RING)
|
||||
{
|
||||
if (!terse)
|
||||
msg("it would be difficult to wrap that around a finger");
|
||||
else
|
||||
msg("not a ring");
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* find out which hand to put it on
|
||||
*/
|
||||
if (is_current(obj))
|
||||
return;
|
||||
|
||||
if (cur_ring[LEFT] == NULL && cur_ring[RIGHT] == NULL)
|
||||
{
|
||||
if ((ring = gethand()) < 0)
|
||||
return;
|
||||
}
|
||||
else if (cur_ring[LEFT] == NULL)
|
||||
ring = LEFT;
|
||||
else if (cur_ring[RIGHT] == NULL)
|
||||
ring = RIGHT;
|
||||
else
|
||||
{
|
||||
if (!terse)
|
||||
msg("you already have a ring on each hand");
|
||||
else
|
||||
msg("wearing two");
|
||||
return;
|
||||
}
|
||||
cur_ring[ring] = obj;
|
||||
|
||||
/*
|
||||
* Calculate the effect it has on the poor guy.
|
||||
*/
|
||||
switch (obj->o_which)
|
||||
{
|
||||
case R_ADDSTR:
|
||||
chg_str(obj->o_arm);
|
||||
break;
|
||||
case R_SEEINVIS:
|
||||
invis_on();
|
||||
break;
|
||||
case R_AGGR:
|
||||
aggravate();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!terse)
|
||||
addmsg("you are now wearing ");
|
||||
msg("%s (%c)", inv_name(obj, TRUE), obj->o_packch);
|
||||
}
|
||||
|
||||
/*
|
||||
* ring_off:
|
||||
* take off a ring
|
||||
*/
|
||||
|
||||
void
|
||||
ring_off()
|
||||
{
|
||||
int ring;
|
||||
THING *obj;
|
||||
|
||||
if (cur_ring[LEFT] == NULL && cur_ring[RIGHT] == NULL)
|
||||
{
|
||||
if (terse)
|
||||
msg("no rings");
|
||||
else
|
||||
msg("you aren't wearing any rings");
|
||||
return;
|
||||
}
|
||||
else if (cur_ring[LEFT] == NULL)
|
||||
ring = RIGHT;
|
||||
else if (cur_ring[RIGHT] == NULL)
|
||||
ring = LEFT;
|
||||
else
|
||||
if ((ring = gethand()) < 0)
|
||||
return;
|
||||
mpos = 0;
|
||||
obj = cur_ring[ring];
|
||||
if (obj == NULL)
|
||||
{
|
||||
msg("not wearing such a ring");
|
||||
return;
|
||||
}
|
||||
if (dropcheck(obj))
|
||||
msg("was wearing %s(%c)", inv_name(obj, TRUE), obj->o_packch);
|
||||
}
|
||||
|
||||
/*
|
||||
* gethand:
|
||||
* Which hand is the hero interested in?
|
||||
*/
|
||||
int
|
||||
gethand()
|
||||
{
|
||||
int c;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (terse)
|
||||
msg("left or right ring? ");
|
||||
else
|
||||
msg("left hand or right hand? ");
|
||||
if ((c = readchar()) == ESCAPE)
|
||||
return -1;
|
||||
mpos = 0;
|
||||
if (c == 'l' || c == 'L')
|
||||
return LEFT;
|
||||
else if (c == 'r' || c == 'R')
|
||||
return RIGHT;
|
||||
if (terse)
|
||||
msg("L or R");
|
||||
else
|
||||
msg("please type L or R");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ring_eat:
|
||||
* How much food does this ring use up?
|
||||
*/
|
||||
int
|
||||
ring_eat(int hand)
|
||||
{
|
||||
THING *ring;
|
||||
int eat;
|
||||
static int uses[] = {
|
||||
1, /* R_PROTECT */ 1, /* R_ADDSTR */
|
||||
1, /* R_SUSTSTR */ -3, /* R_SEARCH */
|
||||
-5, /* R_SEEINVIS */ 0, /* R_NOP */
|
||||
0, /* R_AGGR */ -3, /* R_ADDHIT */
|
||||
-3, /* R_ADDDAM */ 2, /* R_REGEN */
|
||||
-2, /* R_DIGEST */ 0, /* R_TELEPORT */
|
||||
1, /* R_STEALTH */ 1 /* R_SUSTARM */
|
||||
};
|
||||
|
||||
if ((ring = cur_ring[hand]) == NULL)
|
||||
return 0;
|
||||
if ((eat = uses[ring->o_which]) < 0)
|
||||
eat = (rnd(-eat) == 0);
|
||||
if (ring->o_which == R_DIGEST)
|
||||
eat = -eat;
|
||||
return eat;
|
||||
}
|
||||
|
||||
/*
|
||||
* ring_num:
|
||||
* Print ring bonuses
|
||||
*/
|
||||
char *
|
||||
ring_num(THING *obj)
|
||||
{
|
||||
static char buf[10];
|
||||
|
||||
if (!(obj->o_flags & ISKNOW))
|
||||
return "";
|
||||
switch (obj->o_which)
|
||||
{
|
||||
case R_PROTECT:
|
||||
case R_ADDSTR:
|
||||
case R_ADDDAM:
|
||||
case R_ADDHIT:
|
||||
sprintf(buf, " [%s]", num(obj->o_arm, 0, RING));
|
||||
otherwise:
|
||||
return "";
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
449
rip.c
Normal file
449
rip.c
Normal file
@@ -0,0 +1,449 @@
|
||||
/*
|
||||
* File for the fun ends
|
||||
* Death or a total win
|
||||
*
|
||||
* @(#)rip.c 4.57 (Berkeley) 02/05/99
|
||||
*
|
||||
* Rogue: Exploring the Dungeons of Doom
|
||||
* Copyright (C) 1980-1983, 1985, 1999 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
* All rights reserved.
|
||||
*
|
||||
* See the file LICENSE.TXT for full copyright and licensing information.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <ctype.h>
|
||||
#include <fcntl.h>
|
||||
#include <curses.h>
|
||||
#include "rogue.h"
|
||||
#include "score.h"
|
||||
|
||||
static char *rip[] = {
|
||||
" __________\n",
|
||||
" / \\\n",
|
||||
" / REST \\\n",
|
||||
" / IN \\\n",
|
||||
" / PEACE \\\n",
|
||||
" / \\\n",
|
||||
" | |\n",
|
||||
" | |\n",
|
||||
" | killed by a |\n",
|
||||
" | |\n",
|
||||
" | 1980 |\n",
|
||||
" *| * * * | *\n",
|
||||
" ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______\n",
|
||||
0
|
||||
};
|
||||
|
||||
/*
|
||||
* score:
|
||||
* Figure score and post it.
|
||||
*/
|
||||
/* VARARGS2 */
|
||||
|
||||
void
|
||||
score(int amount, int flags, char monst)
|
||||
{
|
||||
SCORE *scp;
|
||||
int i;
|
||||
SCORE *sc2;
|
||||
SCORE *top_ten, *endp;
|
||||
# ifdef MASTER
|
||||
int prflags = 0;
|
||||
# endif
|
||||
void (*fp)(int);
|
||||
unsigned int uid;
|
||||
static char *reason[] = {
|
||||
"killed",
|
||||
"quit",
|
||||
"A total winner",
|
||||
"killed with Amulet"
|
||||
};
|
||||
|
||||
start_score();
|
||||
|
||||
if (flags >= 0
|
||||
#ifdef MASTER
|
||||
|| wizard
|
||||
#endif
|
||||
)
|
||||
{
|
||||
mvaddstr(LINES - 1, 0 , "[Press return to continue]");
|
||||
refresh();
|
||||
wgetnstr(stdscr,prbuf,80);
|
||||
endwin();
|
||||
printf("\n");
|
||||
resetltchars();
|
||||
/*
|
||||
* free up space to "guarantee" there is space for the top_ten
|
||||
*/
|
||||
delwin(stdscr);
|
||||
delwin(curscr);
|
||||
if (hw != NULL)
|
||||
delwin(hw);
|
||||
}
|
||||
|
||||
top_ten = (SCORE *) malloc(numscores * sizeof (SCORE));
|
||||
endp = &top_ten[numscores];
|
||||
for (scp = top_ten; scp < endp; scp++)
|
||||
{
|
||||
scp->sc_score = 0;
|
||||
for (i = 0; i < MAXSTR; i++)
|
||||
scp->sc_name[i] = (unsigned char) rnd(255);
|
||||
scp->sc_flags = RN;
|
||||
scp->sc_level = RN;
|
||||
scp->sc_monster = (unsigned short) RN;
|
||||
scp->sc_uid = RN;
|
||||
}
|
||||
|
||||
signal(SIGINT, SIG_DFL);
|
||||
|
||||
#ifdef MASTER
|
||||
if (wizard)
|
||||
if (strcmp(prbuf, "names") == 0)
|
||||
prflags = 1;
|
||||
else if (strcmp(prbuf, "edit") == 0)
|
||||
prflags = 2;
|
||||
#endif
|
||||
rd_score(top_ten);
|
||||
/*
|
||||
* Insert her in list if need be
|
||||
*/
|
||||
sc2 = NULL;
|
||||
if (!noscore)
|
||||
{
|
||||
uid = md_getuid();
|
||||
for (scp = top_ten; scp < endp; scp++)
|
||||
if (amount > scp->sc_score)
|
||||
break;
|
||||
else if (!allscore && /* only one score per nowin uid */
|
||||
flags != 2 && scp->sc_uid == uid && scp->sc_flags != 2)
|
||||
scp = endp;
|
||||
if (scp < endp)
|
||||
{
|
||||
if (flags != 2 && !allscore)
|
||||
{
|
||||
for (sc2 = scp; sc2 < endp; sc2++)
|
||||
{
|
||||
if (sc2->sc_uid == uid && sc2->sc_flags != 2)
|
||||
break;
|
||||
}
|
||||
if (sc2 >= endp)
|
||||
sc2 = endp - 1;
|
||||
}
|
||||
else
|
||||
sc2 = endp - 1;
|
||||
while (sc2 > scp)
|
||||
{
|
||||
*sc2 = sc2[-1];
|
||||
sc2--;
|
||||
}
|
||||
scp->sc_score = amount;
|
||||
strncpy(scp->sc_name, whoami, MAXSTR);
|
||||
scp->sc_flags = flags;
|
||||
if (flags == 2)
|
||||
scp->sc_level = max_level;
|
||||
else
|
||||
scp->sc_level = level;
|
||||
scp->sc_monster = monst;
|
||||
scp->sc_uid = uid;
|
||||
sc2 = scp;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Print the list
|
||||
*/
|
||||
if (flags != -1)
|
||||
putchar('\n');
|
||||
printf("Top %s %s:\n", Numname, allscore ? "Scores" : "Rogueists");
|
||||
printf(" Score Name\n");
|
||||
for (scp = top_ten; scp < endp; scp++)
|
||||
{
|
||||
if (scp->sc_score) {
|
||||
if (sc2 == scp)
|
||||
md_raw_standout();
|
||||
printf("%2d %5d %s: %s on level %d", (int) (scp - top_ten + 1),
|
||||
scp->sc_score, scp->sc_name, reason[scp->sc_flags],
|
||||
scp->sc_level);
|
||||
if (scp->sc_flags == 0 || scp->sc_flags == 3)
|
||||
printf(" by %s", killname((char) scp->sc_monster, TRUE));
|
||||
#ifdef MASTER
|
||||
if (prflags == 1)
|
||||
{
|
||||
printf(" (%s)", md_getrealname(scp->sc_uid));
|
||||
}
|
||||
else if (prflags == 2)
|
||||
{
|
||||
fflush(stdout);
|
||||
(void) fgets(prbuf,10,stdin);
|
||||
if (prbuf[0] == 'd')
|
||||
{
|
||||
for (sc2 = scp; sc2 < endp - 1; sc2++)
|
||||
*sc2 = *(sc2 + 1);
|
||||
sc2 = endp - 1;
|
||||
sc2->sc_score = 0;
|
||||
for (i = 0; i < MAXSTR; i++)
|
||||
sc2->sc_name[i] = (char) rnd(255);
|
||||
sc2->sc_flags = RN;
|
||||
sc2->sc_level = RN;
|
||||
sc2->sc_monster = (unsigned short) RN;
|
||||
scp--;
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif /* MASTER */
|
||||
printf(".");
|
||||
if (sc2 == scp)
|
||||
md_raw_standend();
|
||||
putchar('\n');
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
/*
|
||||
* Update the list file
|
||||
*/
|
||||
if (sc2 != NULL)
|
||||
{
|
||||
if (lock_sc())
|
||||
{
|
||||
fp = signal(SIGINT, SIG_IGN);
|
||||
wr_score(top_ten);
|
||||
unlock_sc();
|
||||
signal(SIGINT, fp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* death:
|
||||
* Do something really fun when he dies
|
||||
*/
|
||||
|
||||
void
|
||||
death(char monst)
|
||||
{
|
||||
char **dp, *killer;
|
||||
struct tm *lt;
|
||||
static time_t date;
|
||||
struct tm *localtime();
|
||||
|
||||
signal(SIGINT, SIG_IGN);
|
||||
purse -= purse / 10;
|
||||
signal(SIGINT, leave);
|
||||
clear();
|
||||
killer = killname(monst, FALSE);
|
||||
if (!tombstone)
|
||||
{
|
||||
mvprintw(LINES - 2, 0, "Killed by ");
|
||||
killer = killname(monst, FALSE);
|
||||
if (monst != 's' && monst != 'h')
|
||||
printw("a%s ", vowelstr(killer));
|
||||
printw("%s with %d gold", killer, purse);
|
||||
}
|
||||
else
|
||||
{
|
||||
time(&date);
|
||||
lt = localtime(&date);
|
||||
move(8, 0);
|
||||
dp = rip;
|
||||
while (*dp)
|
||||
addstr(*dp++);
|
||||
mvaddstr(17, center(killer), killer);
|
||||
if (monst == 's' || monst == 'h')
|
||||
mvaddch(16, 32, ' ');
|
||||
else
|
||||
mvaddstr(16, 33, vowelstr(killer));
|
||||
mvaddstr(14, center(whoami), whoami);
|
||||
sprintf(prbuf, "%d Au", purse);
|
||||
move(15, center(prbuf));
|
||||
addstr(prbuf);
|
||||
sprintf(prbuf, "%4d", 1900+lt->tm_year);
|
||||
mvaddstr(18, 26, prbuf);
|
||||
}
|
||||
move(LINES - 1, 0);
|
||||
refresh();
|
||||
score(purse, amulet ? 3 : 0, monst);
|
||||
printf("[Press return to continue]");
|
||||
fflush(stdout);
|
||||
(void) fgets(prbuf,10,stdin);
|
||||
my_exit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* center:
|
||||
* Return the index to center the given string
|
||||
*/
|
||||
int
|
||||
center(char *str)
|
||||
{
|
||||
return 28 - (((int)strlen(str) + 1) / 2);
|
||||
}
|
||||
|
||||
/*
|
||||
* total_winner:
|
||||
* Code for a winner
|
||||
*/
|
||||
|
||||
void
|
||||
total_winner()
|
||||
{
|
||||
THING *obj;
|
||||
struct obj_info *op;
|
||||
int worth = 0;
|
||||
int oldpurse;
|
||||
|
||||
clear();
|
||||
standout();
|
||||
addstr(" \n");
|
||||
addstr(" @ @ @ @ @ @@@ @ @ \n");
|
||||
addstr(" @ @ @@ @@ @ @ @ @ \n");
|
||||
addstr(" @ @ @@@ @ @ @ @ @ @@@ @@@@ @@@ @ @@@ @ \n");
|
||||
addstr(" @@@@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ \n");
|
||||
addstr(" @ @ @ @ @ @ @ @@@@ @ @ @@@@@ @ @ @ \n");
|
||||
addstr(" @ @ @ @ @ @@ @ @ @ @ @ @ @ @ @ @ \n");
|
||||
addstr(" @@@ @@@ @@ @ @ @ @@@@ @@@@ @@@ @@@ @@ @ \n");
|
||||
addstr(" \n");
|
||||
addstr(" Congratulations, you have made it to the light of day! \n");
|
||||
standend();
|
||||
addstr("\nYou have joined the elite ranks of those who have escaped the\n");
|
||||
addstr("Dungeons of Doom alive. You journey home and sell all your loot at\n");
|
||||
addstr("a great profit and are admitted to the Fighters' Guild.\n");
|
||||
mvaddstr(LINES - 1, 0, "--Press space to continue--");
|
||||
refresh();
|
||||
wait_for(' ');
|
||||
clear();
|
||||
mvaddstr(0, 0, " Worth Item\n");
|
||||
oldpurse = purse;
|
||||
for (obj = pack; obj != NULL; obj = next(obj))
|
||||
{
|
||||
switch (obj->o_type)
|
||||
{
|
||||
case FOOD:
|
||||
worth = 2 * obj->o_count;
|
||||
when WEAPON:
|
||||
worth = weap_info[obj->o_which].oi_worth;
|
||||
worth *= 3 * (obj->o_hplus + obj->o_dplus) + obj->o_count;
|
||||
obj->o_flags |= ISKNOW;
|
||||
when ARMOR:
|
||||
worth = arm_info[obj->o_which].oi_worth;
|
||||
worth += (9 - obj->o_arm) * 100;
|
||||
worth += (10 * (a_class[obj->o_which] - obj->o_arm));
|
||||
obj->o_flags |= ISKNOW;
|
||||
when SCROLL:
|
||||
worth = scr_info[obj->o_which].oi_worth;
|
||||
worth *= obj->o_count;
|
||||
op = &scr_info[obj->o_which];
|
||||
if (!op->oi_know)
|
||||
worth /= 2;
|
||||
op->oi_know = TRUE;
|
||||
when POTION:
|
||||
worth = pot_info[obj->o_which].oi_worth;
|
||||
worth *= obj->o_count;
|
||||
op = &pot_info[obj->o_which];
|
||||
if (!op->oi_know)
|
||||
worth /= 2;
|
||||
op->oi_know = TRUE;
|
||||
when RING:
|
||||
op = &ring_info[obj->o_which];
|
||||
worth = op->oi_worth;
|
||||
if (obj->o_which == R_ADDSTR || obj->o_which == R_ADDDAM ||
|
||||
obj->o_which == R_PROTECT || obj->o_which == R_ADDHIT)
|
||||
{
|
||||
if (obj->o_arm > 0)
|
||||
worth += obj->o_arm * 100;
|
||||
else
|
||||
worth = 10;
|
||||
}
|
||||
if (!(obj->o_flags & ISKNOW))
|
||||
worth /= 2;
|
||||
obj->o_flags |= ISKNOW;
|
||||
op->oi_know = TRUE;
|
||||
when STICK:
|
||||
op = &ws_info[obj->o_which];
|
||||
worth = op->oi_worth;
|
||||
worth += 20 * obj->o_charges;
|
||||
if (!(obj->o_flags & ISKNOW))
|
||||
worth /= 2;
|
||||
obj->o_flags |= ISKNOW;
|
||||
op->oi_know = TRUE;
|
||||
when AMULET:
|
||||
worth = 1000;
|
||||
}
|
||||
if (worth < 0)
|
||||
worth = 0;
|
||||
printw("%c) %5d %s\n", obj->o_packch, worth, inv_name(obj, FALSE));
|
||||
purse += worth;
|
||||
}
|
||||
printw(" %5d Gold Pieces ", oldpurse);
|
||||
refresh();
|
||||
score(purse, 2, ' ');
|
||||
my_exit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* killname:
|
||||
* Convert a code to a monster name
|
||||
*/
|
||||
char *
|
||||
killname(char monst, bool doart)
|
||||
{
|
||||
struct h_list *hp;
|
||||
char *sp;
|
||||
bool article;
|
||||
static struct h_list nlist[] = {
|
||||
{'a', "arrow", TRUE},
|
||||
{'b', "bolt", TRUE},
|
||||
{'d', "dart", TRUE},
|
||||
{'h', "hypothermia", FALSE},
|
||||
{'s', "starvation", FALSE},
|
||||
{'\0'}
|
||||
};
|
||||
|
||||
if (isupper(monst))
|
||||
{
|
||||
sp = monsters[monst-'A'].m_name;
|
||||
article = TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
sp = "Wally the Wonder Badger";
|
||||
article = FALSE;
|
||||
for (hp = nlist; hp->h_ch; hp++)
|
||||
if (hp->h_ch == monst)
|
||||
{
|
||||
sp = hp->h_desc;
|
||||
article = hp->h_print;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (doart && article)
|
||||
sprintf(prbuf, "a%s ", vowelstr(sp));
|
||||
else
|
||||
prbuf[0] = '\0';
|
||||
strcat(prbuf, sp);
|
||||
return prbuf;
|
||||
}
|
||||
|
||||
/*
|
||||
* death_monst:
|
||||
* Return a monster appropriate for a random death.
|
||||
*/
|
||||
char
|
||||
death_monst()
|
||||
{
|
||||
static char poss[] =
|
||||
{
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
||||
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
|
||||
'Y', 'Z', 'a', 'b', 'h', 'd', 's',
|
||||
' ' /* This is provided to generate the "Wally the Wonder Badger"
|
||||
message for killer */
|
||||
};
|
||||
|
||||
return poss[rnd(sizeof poss / sizeof (char))];
|
||||
}
|
||||
96
rogue.6.in
Normal file
96
rogue.6.in
Normal file
@@ -0,0 +1,96 @@
|
||||
.\"
|
||||
.\" @(#)rogue.6 6.2 (Berkeley) 5/6/86
|
||||
.\"
|
||||
.\" Rogue: Exploring the Dungeons of Doom
|
||||
.\" Copyright (C) 1980-1983, 1985, 1986 Michael Toy, Ken Arnold and Glenn Wichman
|
||||
.\" All rights reserved.
|
||||
.\"
|
||||
.\" See the file LICENSE.TXT for full copyright and licensing information.
|
||||
.\"
|
||||
.TH ROGUE 6 "May 6, 1986"
|
||||
.UC 4
|
||||
.SH NAME
|
||||
rogue \- Exploring The Dungeons of Doom
|
||||
.SH SYNOPSIS
|
||||
.B @PROGRAM@
|
||||
[
|
||||
.B \-r
|
||||
]
|
||||
[
|
||||
.I save_file
|
||||
]
|
||||
[
|
||||
.B \-s
|
||||
]
|
||||
[
|
||||
.B \-d
|
||||
]
|
||||
.SH DESCRIPTION
|
||||
.PP
|
||||
.I Rogue
|
||||
is a computer fantasy game with a new twist. It is crt oriented and the
|
||||
object of the game is to survive the attacks of various monsters and get
|
||||
a lot of gold, rather than the puzzle solving orientation of most computer
|
||||
fantasy games.
|
||||
.PP
|
||||
To get started you really only need to know two commands. The command
|
||||
.B ?
|
||||
will give you a list of the available commands and the command
|
||||
.B /
|
||||
will identify the things you see on the screen.
|
||||
.PP
|
||||
To win the game (as opposed to merely playing to beat other people's high
|
||||
scores) you must locate the Amulet of Yendor which is somewhere below
|
||||
the 20th level of the dungeon and get it out. Nobody has achieved this
|
||||
yet and if somebody does, they will probably go down in history as a hero
|
||||
among heroes.
|
||||
.PP
|
||||
When the game ends, either by your death, when you quit, or if you (by
|
||||
some miracle) manage to win,
|
||||
.I rogue
|
||||
will give you a list of the top-ten scorers. The scoring is based entirely
|
||||
upon how much gold you get. There is a 10% penalty for getting yourself
|
||||
killed.
|
||||
.PP
|
||||
If
|
||||
.I save_file
|
||||
is specified,
|
||||
rogue will be restored from the specified saved game file.
|
||||
If the
|
||||
.B \-r
|
||||
option is used, the save game file is presumed to be the default.
|
||||
.PP
|
||||
The
|
||||
.B \-s
|
||||
option will print out the list of scores.
|
||||
.PP
|
||||
The
|
||||
.B \-d
|
||||
option will kill you and try to add you to the score file.
|
||||
.PP
|
||||
For more detailed directions, read the document
|
||||
.I "A Guide to the Dungeons of Doom."
|
||||
.SH AUTHORS
|
||||
Michael C. Toy,
|
||||
Kenneth C. R. C. Arnold,
|
||||
Glenn Wichman
|
||||
.SH FILES
|
||||
.DT
|
||||
.ta \w'@SCOREFILE@\ \ \ 'u
|
||||
@SCOREFILE@ Score file
|
||||
.br
|
||||
\fB~\fP/rogue.save Default save file
|
||||
.SH SEE ALSO
|
||||
Michael C. Toy
|
||||
and
|
||||
Kenneth C. R. C. Arnold,
|
||||
.I "A guide to the Dungeons of Doom"
|
||||
.SH BUGS
|
||||
.PP
|
||||
Probably infinite
|
||||
(although countably infinite).
|
||||
However,
|
||||
that Ice Monsters sometimes transfix you permanently is
|
||||
.I not
|
||||
a bug.
|
||||
It's a feature.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user