go: port combat, chase driver, traps, zapping, death and scores

- fight.c in full: fight/attack with all eight special monster attacks
  (aquator rust, ice freeze, rattlesnake poison, wraith/vampire drains,
  flytrap hold, leprechaun/nymph theft), swing, roll_em dice parsing
  (with a C-semantics atoi — Go's strconv rejects '1x4/...'), hit/miss
  message variants, killed, remove_mon
- chase.c completed: runners, move_monst, relocate, do_chase with
  dragon breath, chase target selection (C's dead oroom comparison in
  relocate is preserved as-is)
- move.c in full: do_move with passgo turning, all eight traps,
  rndmove, rust_armor
- sticks.c completed: do_zap (all 14 sticks), drain, fire_bolt with
  wall bounces; weapons.c completed: missile/do_motion/fall/wield
- rip.c: death tombstone, total_winner, killname; C exit() becomes a
  gameEnd panic that Run will recover
- score.go: top-ten scoreboard as a gob file with lock-file protocol
  replacing the XOR-encrypted C format
- wizard.c: whatis/set_know/teleport; daemons.c stomach
- monster bestiary moved to per-game state (C mutates the flytrap
  damage string during play)

Tests: combat math, kill/removal bookkeeping, monster chase pursuit,
death unwinding, scripted-input headless terminal.
This commit is contained in:
2026-07-06 19:23:45 +02:00
parent a69ef7dc04
commit 3c5add87cd
13 changed files with 2315 additions and 24 deletions

View File

@@ -1,8 +1,265 @@
package game
// chase.c — the navigation and visibility half. The chase driver (runners,
// move_monst, do_chase, chase) arrives with the combat phase, since it
// attacks the player and fires dragon bolts.
// 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 {
if !tp.On(IsHeld) && tp.On(IsRun) {
origPos := tp.Pos
wastarget := tp.On(IsTarget)
if g.moveMonst(tp) == -1 {
continue
}
if tp.On(IsFly) && distCp(g.Player.Pos, tp.Pos) >= 3 {
if g.moveMonst(tp) == -1 {
continue
}
}
if wastarget && origPos != tp.Pos {
tp.Flags.Clear(IsTarget)
g.ToDeath = false
}
}
}
if g.HasHit {
g.endmsg()
g.HasHit = false
}
}
// moveMonst executes a single turn of running for a monster (chase.c
// move_monst). Returns -1 if the monster died or left the level.
func (g *RogueGame) moveMonst(tp *Monster) int {
if !tp.On(IsSlow) || tp.Turn {
if g.doChase(tp) == -1 {
return -1
}
}
if tp.On(IsHaste) {
if g.doChase(tp) == -1 {
return -1
}
}
tp.Turn = !tp.Turn
return 0
}
// 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.setOldch(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(SeeMonst) {
g.standout()
g.addch(th.Type)
g.standend()
}
}
// doChase makes one thing chase another (chase.c do_chase). Returns -1 if
// the chaser died in the attempt.
func (g *RogueGame) doChase(th *Monster) int {
p := &g.Player
stoprun := false // true means we are there
mindist := 32767
rer := th.Room // find room of chaser
if th.On(IsGreed) && 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)
}
// We don't count doors as inside rooms for this routine
door := g.Level.Char(th.Pos.Y, th.Pos.X) == Door
var this Coord
over:
// 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 {
for i := range rer.Exits {
curdist := distCp(*th.Dest, rer.Exits[i])
if curdist < mindist {
this = rer.Exits[i]
mindist = curdist
}
}
if door {
rer = &g.Level.Passages[*g.Level.FlagsAt(th.Pos.Y, th.Pos.X)&FPNum]
door = false
goto over
}
} else {
this = *th.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.Type == 'D' && (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)) &&
distCp(th.Pos, p.Pos) <= BoltLength*BoltLength &&
!th.On(IsCanc) && g.rnd(dragonShot) == 0 {
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(IsTarget) {
g.ToDeath = false
g.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 !g.chase(th, this) {
if this == p.Pos {
return g.attack(th)
} else if this == *th.Dest {
for _, obj := range g.Level.Objects {
if th.Dest == &obj.Pos {
detachObj(&g.Level.Objects, obj)
attachObj(&th.Pack, obj)
if th.Room.Flags.Has(IsGone) {
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
}
}
if th.Type != 'F' {
stoprun = true
}
}
} else {
if th.Type == 'F' {
return 0
}
}
g.relocate(th, g.chRet)
// And stop running if need be
if stoprun && th.Pos == *th.Dest {
th.Flags.Clear(IsRun)
}
return 0
}
// 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 {
p := &g.Player
er := tp.Pos
plcnt := 1
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(IsHuh) && 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.rndmove(&tp.Creature)
curdist = distCp(g.chRet, ee)
// Small chance that it will become un-confused
if g.rnd(20) == 0 {
tp.Flags.Clear(IsHuh)
}
} else {
// Otherwise, find the empty spot next to the chaser that is
// closest to the chasee. This will eventually hold where we move
// to get closer. If we can't find an empty spot, we stay where we
// are.
curdist = distCp(er, ee)
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++ {
tryp := Coord{X: x, Y: y}
if !g.diagOk(er, tryp) {
continue
}
ch := g.Level.VisibleChar(y, x)
if stepOk(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 {
var found *Object
for _, obj := range g.Level.Objects {
if y == obj.Pos.Y && x == obj.Pos.X {
found = obj
break
}
}
if found != nil && found.Which == SScare {
continue
}
}
// It can also be a Xeroc, which we shouldn't step on
if m := g.Level.MonsterAt(y, x); m != nil && m.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 := distance(y, x, ee.Y, ee.X)
if thisdist < curdist {
plcnt = 1
g.chRet = tryp
curdist = thisdist
} else if thisdist == curdist {
if plcnt++; g.rnd(plcnt) == 0 {
g.chRet = tryp
curdist = thisdist
}
}
}
}
}
}
return curdist != 0 && g.chRet != p.Pos
}
// setOldch sets the oldch character for the monster (chase.c set_oldch).
func (g *RogueGame) setOldch(tp *Monster, cp Coord) {
@@ -113,7 +370,7 @@ func (g *RogueGame) cansee(y, x int) bool {
// findDest finds the proper destination for the monster (chase.c
// find_dest).
func (g *RogueGame) findDest(tp *Monster) *Coord {
prob := monsterTable[tp.Type-'A'].Carry
prob := g.Monsters[tp.Type-'A'].Carry
if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) {
return &g.Player.Pos
}

View File

@@ -10,6 +10,10 @@ func (g *RogueGame) runDaemon(id DaemonID, arg int) {
g.rollwand(arg)
case DDoctor:
g.doctor(arg)
case DStomach:
g.stomach(arg)
case DRunners:
g.runners(arg)
case DSwander:
g.swander(arg)
case DNohaste:
@@ -120,6 +124,57 @@ func (g *RogueGame) nohaste(int) {
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 {
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.addmsg("%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"))
} else {
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 capabilites",
"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"))
}
}
}
if p.HungryState != origHungry {
p.Flags.Clear(IsRun)
g.Running = false
g.ToDeath = false
g.Count = 0
}
}
// comeDown takes the hero down off her acid trip (daemons.c come_down).
func (g *RogueGame) comeDown(int) {
p := &g.Player

View File

@@ -1,7 +1,49 @@
package game
// fight.c — combat. setMname arrives first (monster wake-ups need it); the
// combat resolution functions come with the combat phase.
import (
"fmt"
"strconv"
"strings"
)
// fight.c — all the fighting gets done here.
// hNames are the strings for hitting; the first four are used when the
// player strikes, the second four for monsters (fight.c h_names).
var 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 are the strings for missing (fight.c m_names).
var mNames = [8]string{
" miss",
" swing and miss",
" barely miss",
" don't hit",
" misses",
" swings and misses",
" barely misses",
" doesn't hit",
}
// strPlus adjusts hit probabilities due to strength (fight.c str_plus).
var 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 adjusts damage done due to strength (fight.c add_dam).
var 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,
}
// setMname returns the monster name for the given monster (fight.c
// set_mname).
@@ -20,9 +62,517 @@ func (g *RogueGame) setMname(tp *Monster) string {
} else {
ch -= 'A'
}
mname = monsterTable[ch].Name
mname = g.Monsters[ch].Name
} else {
mname = monsterTable[tp.Type-'A'].Name
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)
// Let him know it was really a xeroc (if it was one).
if tp.Type == 'X' && tp.Disguise != 'X' && !p.On(IsBlind) {
tp.Disguise = 'X'
if p.On(IsHalu) {
g.mvaddch(tp.Pos.Y, tp.Pos.X, byte(g.rnd(26)+'A'))
}
g.msg("%s", g.chooseStr("heavy! That's a nasty critter!",
"wait! That's a xeroc!"))
if !thrown {
return false
}
}
mname := g.setMname(tp)
didHit := false
g.HasHit = g.Options.Terse && !g.ToDeath
if g.rollEm(&p.Creature, &tp.Creature, weap, thrown) {
didHit = false
if thrown {
g.thunk(weap, mname, g.Options.Terse)
} else {
g.hit("", mname, g.Options.Terse)
}
if p.On(CanHuh) {
didHit = true
tp.Flags.Set(IsHuh)
p.Flags.Clear(CanHuh)
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 didHit && !p.On(IsBlind) {
g.msg("%s appears confused", mname)
}
didHit = true
} else {
if thrown {
g.bounce(weap, mname, g.Options.Terse)
} else {
g.miss("", mname, g.Options.Terse)
}
}
return didHit
}
// attack has the monster attack the player (fight.c attack). Returns -1 if
// the monster removed itself from the level during its own attack.
func (g *RogueGame) attack(mp *Monster) int {
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(IsTarget) {
g.ToDeath = false
g.Kamikaze = false
}
if mp.Type == 'X' && mp.Disguise != 'X' && !p.On(IsBlind) {
mp.Disguise = 'X'
if p.On(IsHalu) {
g.mvaddch(mp.Pos.Y, mp.Pos.X, byte(g.rnd(26)+'A'))
}
}
mname := g.setMname(mp)
oldhp := p.Stats.HP
removed := false
if g.rollEm(&mp.Creature, &p.Creature, nil, false) {
if mp.Type != 'I' {
if g.HasHit {
g.addmsg(". ")
}
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(IsCanc) {
switch mp.Type {
case 'A':
// If an aquator hits, you can lose armor class.
g.rustArmor(p.CurArmor)
case 'I':
// The ice monster freezes you
p.Flags.Clear(IsRun)
if g.NoCommand == 0 {
g.addmsg("you are frozen")
if !g.Options.Terse {
g.addmsg(" by the %s", mname)
}
g.endmsg()
}
g.NoCommand += g.rnd(2) + 2
if g.NoCommand > BoreLevel {
g.death('h')
}
case 'R':
// Rattlesnakes have poisonous bites
if !g.save(VsPoison) {
if !p.IsWearing(RSustStr) {
g.chgStr(-1)
if !g.Options.Terse {
g.msg("you feel a bite in your leg and now feel weaker")
} else {
g.msg("a bite has weakened you")
}
} else if !g.ToDeath {
if !g.Options.Terse {
g.msg("a bite momentarily weakens you")
} else {
g.msg("bite has no effect")
}
}
}
case 'W', 'V':
// Wraiths might drain energy levels, and Vampires can
// steal max_hp
chance := 30
if mp.Type == 'W' {
chance = 15
}
if g.rnd(100) < chance {
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 = 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")
}
case 'F':
// Venus Flytrap stops the poor guy from moving
p.Flags.Set(IsHeld)
p.VfHit++
g.Monsters['F'-'A'].Stats.Dmg = fmt.Sprintf("%dx1", p.VfHit)
if p.Stats.HP--; p.Stats.HP <= 0 {
g.death('F')
}
case 'L':
// Leprechaun steals some gold
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)
removed = true
if p.Purse != lastpurse {
g.msg("your purse feels lighter")
}
case 'N':
// Nymphs steal a magic item; look through the pack and
// pick out one we like.
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] &&
obj.isMagic() {
if nobj++; g.rnd(nobj) == 0 {
steal = obj
}
}
}
if steal != nil {
g.removeMon(mp.Pos, g.Level.MonsterAt(mp.Pos.Y, mp.Pos.X), false)
removed = true
g.leavePack(steal, false, false)
g.msg("she stole %s!", g.invName(steal, true))
}
}
}
} else if mp.Type != 'I' {
if g.HasHit {
g.addmsg(". ")
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)
}
if g.Options.FightFlush && !g.ToDeath {
g.flushType()
}
g.Count = 0
g.status()
if removed {
return -1
}
return 0
}
// 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
}
// rollEm rolls several attacks (fight.c roll_em).
func (g *RogueGame) rollEm(thatt, thdef *Creature, weap *Object, hurl bool) bool {
p := &g.Player
att := &thatt.Stats
def := &thdef.Stats
var cp string
var hplus, dplus int
if weap == nil {
cp = att.Dmg
} else {
hplus = weap.HPlus
dplus = weap.DPlus
if weap == p.CurWeapon {
if p.IsRing(Left, RAddDam) {
dplus += p.CurRing[Left].Arm
} else if p.IsRing(Left, RAddHit) {
hplus += p.CurRing[Left].Arm
}
if p.IsRing(Right, RAddDam) {
dplus += p.CurRing[Right].Arm
} else if p.IsRing(Right, RAddHit) {
hplus += p.CurRing[Right].Arm
}
}
cp = weap.Damage
if hurl {
if weap.Flags.Has(IsMissl) && p.CurWeapon != nil &&
p.CurWeapon.Which == weap.Launch {
cp = weap.HurlDmg
hplus += p.CurWeapon.HPlus
dplus += p.CurWeapon.DPlus
} else if weap.Launch < 0 {
cp = weap.HurlDmg
}
}
}
// 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(IsRun) {
hplus += 4
}
defArm := def.Arm
if def == &p.Stats {
if p.CurArmor != nil {
defArm = p.CurArmor.Arm
}
if p.IsRing(Left, RProtect) {
defArm -= p.CurRing[Left].Arm
}
if p.IsRing(Right, RProtect) {
defArm -= p.CurRing[Right].Arm
}
}
didHit := false
for cp != "" {
ndice := cAtoi(cp)
xi := strings.IndexByte(cp, 'x')
if xi < 0 {
break
}
cp = cp[xi+1:]
nsides := cAtoi(cp)
if g.swing(att.Lvl, defArm, hplus+strPlus[att.Str]) {
proll := g.roll(ndice, nsides)
damage := dplus + proll + addDam[att.Str]
if damage > 0 {
def.HP -= damage
}
didHit = true
}
si := strings.IndexByte(cp, '/')
if si < 0 {
break
}
cp = cp[si+1:]
}
return didHit
}
// 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.Type == Weapon {
g.addmsg("the %s hits ", g.Items.Weapons[weap.Which].Name)
} else {
g.addmsg("you hit ")
}
g.addmsg("%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.addmsg("%s", prname(er, true))
var s string
if g.Options.Terse {
s = " hit"
} else {
i := g.rnd(4)
if er != "" {
i += 4
}
s = hNames[i]
}
g.addmsg("%s", s)
if !g.Options.Terse {
g.addmsg("%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.addmsg("%s", prname(er, true))
i := 0
if !g.Options.Terse {
i = g.rnd(4)
}
if er != "" {
i += 4
}
g.addmsg("%s", mNames[i])
if !g.Options.Terse {
g.addmsg(" %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.Type == Weapon {
g.addmsg("the %s misses ", g.Items.Weapons[weap.Which].Name)
} else {
g.addmsg("you missed ")
}
g.addmsg("%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)
detachMon(&g.Level.Monsters, tp)
if tp.On(IsTarget) {
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
// If the monster was a venus flytrap, un-hold him
switch tp.Type {
case 'F':
p.Flags.Clear(IsHeld)
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = "000x0"
case 'L':
pos, ok := g.fallpos(tp.Pos)
if ok {
tp.Room.Gold = pos
}
if ok && g.Depth >= g.MaxDepth {
gold := newObject()
gold.Type = Gold
gold.SetGoldVal(g.goldCalc())
if g.save(VsMagic) {
gold.Arm += g.goldCalc() + g.goldCalc() + g.goldCalc() + g.goldCalc()
}
attachObj(&tp.Pack, gold)
}
}
// Get rid of the monster.
mname := g.setMname(tp)
g.removeMon(tp.Pos, tp, true)
if pr {
if g.HasHit {
g.addmsg(". Defeated ")
g.HasHit = false
} else {
if !g.Options.Terse {
g.addmsg("you have ")
}
g.addmsg("defeated ")
}
g.msg("%s", mname)
}
// Do adjustments if he went up a level
g.checkLevel()
if g.Options.FightFlush {
g.flushType()
}
}
// 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()
}
}

118
game/fight_test.go Normal file
View File

@@ -0,0 +1,118 @@
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 := NewGame(Config{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) {
g := mkGame(t, 42)
att := &Creature{Stats: Stats{Str: 16, Lvl: 20, Dmg: "1x4/1x4/1x4"}}
def := &Creature{Stats: Stats{Arm: 10, HP: 1000}}
def.Flags.Set(IsRun)
// 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.rollEm(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) {
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) {
g := mkGame(t, 9)
tp := spawnAdjacent(g, 'T') // troll: 1x8/1x8/2x6
tp.Stats.Lvl = 20 // always hits
tp.Flags.Clear(IsCanc)
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 TestDeathUnwindsWithGameEnd(t *testing.T) {
g := mkGame(t, 11)
defer func() {
r := recover()
if _, ok := r.(gameEnd); !ok {
t.Fatalf("death did not unwind with gameEnd, got %v", r)
}
if g.Playing {
t.Error("still playing after death")
}
}()
g.Options.Tombstone = false
g.death('K')
}
func TestRunnersChaseHero(t *testing.T) {
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(IsRun)
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) {
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")
}
}

View File

@@ -99,6 +99,7 @@ type RogueGame struct {
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
@@ -121,6 +122,10 @@ type RogueGame struct {
// 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
@@ -159,6 +164,7 @@ func NewGame(cfg Config) *RogueGame {
}
// TODO(port): parse cfg.RogueOpts once options.go lands.
g.Monsters = monsterTable
g.Items.Group = 2 // weapons.c: int group = 2
for i := range g.Level.Passages {
g.Level.Passages[i].Flags = IsGone | IsDark
@@ -187,8 +193,8 @@ func (g *RogueGame) quit(int) {
g.scr.Std.MvPrintw(NumLines-2, 0, "You quit with %d gold pieces", g.Player.Purse)
g.move(NumLines-1, 0)
g.refresh()
// TODO(port): score(purse, 1, 0) once rip.go lands.
g.gameOver()
g.score(g.Player.Purse, 1, 0)
g.myExit(0)
return
}
g.move(0, 0)
@@ -200,9 +206,3 @@ func (g *RogueGame) quit(int) {
g.Count = 0
g.ToDeath = false
}
// gameOver ends the game loop; it replaces the C exit() calls so that Run
// can unwind normally and restore the terminal.
func (g *RogueGame) gameOver() {
g.Playing = false
}

View File

@@ -50,7 +50,7 @@ func (g *RogueGame) newMonster(tp *Monster, typ byte, cp Coord) {
tp.OldCh = g.inch()
tp.Room = g.roomin(cp)
g.Level.SetMonsterAt(cp.Y, cp.X, tp)
mp := &monsterTable[tp.Type-'A']
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
@@ -165,7 +165,7 @@ func (g *RogueGame) wakeMonster(y, x int) *Monster {
// 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) < monsterTable[tp.Type-'A'].Carry {
if g.Depth >= g.MaxDepth && g.rnd(100) < g.Monsters[tp.Type-'A'].Carry {
attachObj(&tp.Pack, g.newThing())
}
}

View File

@@ -1,7 +1,180 @@
package game
// move.c — hero movement. doorOpen arrives first (room entry needs it);
// the movement commands and traps come with the UI phase.
// move.c — hero movement commands.
// doRun starts the hero running (move.c do_run).
func (g *RogueGame) doRun(ch byte) {
g.Running = true
g.After = false
g.RunCh = ch
}
// doMove checks that a move is legal and handles the consequences —
// fighting, picking up, etc. (move.c do_move).
func (g *RogueGame) doMove(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(IsHuh) && g.rnd(5) != 0 {
nh = g.rndmove(&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}
}
over:
// Check if he tried to move off the screen or make an illegal diagonal
// move, and stop him if he did.
hitBound := nh.X < 0 || nh.X >= NumCols || nh.Y <= 0 || nh.Y >= NumLines-1
var ch byte
var fl PlaceFlags
if !hitBound {
if !g.diagOk(p.Pos, nh) {
g.After = false
g.Running = false
return
}
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(IsLevit) {
ch = Trap
g.Level.SetChar(nh.Y, nh.X, Trap)
g.Level.FlagsAt(nh.Y, nh.X).Set(FReal)
}
} else if p.On(IsHeld) && ch != 'F' {
g.msg("you are being held")
return
}
}
if hitBound {
ch = ' ' // fall into the wall case below
}
switch ch {
case ' ', '|', '-':
if g.Options.PassGo && g.Running && p.Room.Flags.Has(IsGone) &&
!p.On(IsBlind) {
var b1, b2 bool
switch g.RunCh {
case 'h', 'l':
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 {
if b1 {
g.RunCh = 'k'
dy = -1
} else {
g.RunCh = 'j'
dy = 1
}
dx = 0
g.turnref()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
goto over
}
case 'j', 'k':
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 {
if b1 {
g.RunCh = 'h'
dx = -1
} else {
g.RunCh = 'l'
dx = 1
}
dy = 0
g.turnref()
nh = Coord{Y: p.Pos.Y + dy, X: p.Pos.X + dx}
goto over
}
}
}
g.Running = false
g.After = false
case Door:
g.Running = false
if g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Has(FPass) {
g.enterRoom(nh)
}
g.moveStuff(nh, fl)
case Trap:
tr := g.beTrapped(nh)
if tr == TDoor || tr == TTelep {
return
}
g.moveStuff(nh, fl)
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)
g.moveStuff(nh, fl)
case Floor:
if !fl.Has(FReal) {
g.beTrapped(p.Pos)
}
g.moveStuff(nh, fl)
default:
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.moveStuff(nh, fl)
}
}
}
// moveStuff is the move_stuff label in do_move: complete the step.
func (g *RogueGame) moveStuff(nh Coord, fl PlaceFlags) {
p := &g.Player
g.mvaddch(p.Pos.Y, p.Pos.X, g.floorAt())
if fl.Has(FPass) && 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|FPass) == (FReal|FPass)
}
// turnref decides whether to refresh at a passage turning (move.c turnref).
func (g *RogueGame) turnref() {
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).
@@ -17,3 +190,152 @@ func (g *RogueGame) doorOpen(rp *Room) {
}
}
}
// beTrapped makes him pay for stepping on a trap (move.c be_trapped).
func (g *RogueGame) beTrapped(tc Coord) int {
p := &g.Player
if p.On(IsLevit) {
return TRust // 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 := int(pp.Flags & FTMask)
pp.Flags.Set(FSeen)
switch tr {
case TDoor:
g.Depth++
g.NewLevel()
g.msg("you fell into a trap!")
case TBear:
g.NoMove += g.spread(3) // BEARTIME
g.msg("you are caught in a bear trap")
case TMyst:
switch g.rnd(11) {
case 0:
g.msg("you are suddenly in a parallel dimension")
case 1:
g.msg("the light in here suddenly seems %s", rainbow[g.rnd(len(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", rainbow[g.rnd(len(rainbow))])
case 5:
g.msg("a spike shoots past your ear!")
case 6:
g.msg("%s sparks dance across your armor", rainbow[g.rnd(len(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!", rainbow[g.rnd(len(rainbow))])
}
case TSleep:
g.NoCommand += g.spread(5) // SLEEPTIME
p.Flags.Clear(IsRun)
g.msg("a strange white mist envelops you and you fall asleep")
case TArrow:
if g.swing(p.Stats.Lvl-1, p.Stats.Arm, 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, Arrow)
arrow.Count = 1
arrow.Pos = p.Pos
g.fall(arrow, false)
g.msg("an arrow shoots past you")
}
case TTelep:
// 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)
case TDart:
if !g.swing(p.Stats.Lvl+1, p.Stats.Arm, 1) {
g.msg("a small dart whizzes by your ear and vanishes")
} else {
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(RSustStr) && !g.save(VsPoison) {
g.chgStr(-1)
}
g.msg("a small dart just hit you in the shoulder")
}
case TRust:
g.msg("a gush of water hits you on the head")
g.rustArmor(p.CurArmor)
}
g.flushType()
return tr
}
// rndmove moves in a random direction if the monster/person is confused
// (move.c rndmove).
func (g *RogueGame) rndmove(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.Which == SScare {
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.Type != Armor || arm.Which == Leather ||
arm.Arm >= 9 {
return
}
if arm.Flags.Has(IsProt) || g.Player.IsWearing(RSustArm) {
if !g.ToDeath {
g.msg("the rust vanishes instantly")
}
} else {
arm.Arm++
if !g.Options.Terse {
g.msg("your armor appears to be weaker now. Oh my!")
} else {
g.msg("your armor weakens")
}
}
}

229
game/rip.go Normal file
View File

@@ -0,0 +1,229 @@
package game
import (
"fmt"
"time"
)
// rip.c — the fun ends: death or a total win.
//
// The C functions here call exit(); the port panics with a gameEnd sentinel
// that Run recovers, so the terminal is restored by normal unwinding.
// gameEnd is the sentinel carried by the panic that replaces my_exit().
type gameEnd struct{ status int }
// myExit leaves the process properly (main.c my_exit): it unwinds to Run.
func (g *RogueGame) myExit(st int) {
g.Playing = false
panic(gameEnd{status: st})
}
var ripArt = []string{
" __________",
" / \\",
" / REST \\",
" / IN \\",
" / PEACE \\",
" / \\",
" | |",
" | |",
" | killed by a |",
" | |",
" | 1980 |",
" *| * * * | *",
" ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______",
}
// 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.MvPrintw(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 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(0)
}
// 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 := 0
it := &g.Items
switch obj.Type {
case Food:
worth = 2 * obj.Count
case Weapon:
worth = it.Weapons[obj.Which].Worth
worth *= 3*(obj.HPlus+obj.DPlus) + obj.Count
obj.Flags.Set(IsKnow)
case Armor:
worth = it.Armors[obj.Which].Worth
worth += (9 - obj.Arm) * 100
worth += 10 * (aClass[obj.Which] - obj.Arm)
obj.Flags.Set(IsKnow)
case Scroll:
op := &it.Scrolls[obj.Which]
worth = op.Worth * obj.Count
if !op.Know {
worth /= 2
}
op.Know = true
case Potion:
op := &it.Potions[obj.Which]
worth = op.Worth * obj.Count
if !op.Know {
worth /= 2
}
op.Know = true
case Ring:
op := &it.Rings[obj.Which]
worth = op.Worth
if obj.Which == RAddStr || obj.Which == RAddDam ||
obj.Which == RProtect || obj.Which == RAddHit {
if obj.Arm > 0 {
worth += obj.Arm * 100
} else {
worth = 10
}
}
if !obj.Flags.Has(IsKnow) {
worth /= 2
}
obj.Flags.Set(IsKnow)
op.Know = true
case Stick:
op := &it.Sticks[obj.Which]
worth = op.Worth
worth += 20 * obj.Charges()
if !obj.Flags.Has(IsKnow) {
worth /= 2
}
obj.Flags.Set(IsKnow)
op.Know = true
case Amulet:
worth = 1000
}
if worth < 0 {
worth = 0
}
g.scr.Std.MvPrintw(line, 0, "%c) %5d %s", obj.PackCh, worth,
g.invName(obj, false))
line++
p.Purse += worth
}
g.scr.Std.MvPrintw(line, 0, " %5d Gold Pieces ", oldpurse)
g.refresh()
g.score(p.Purse, 2, ' ')
g.myExit(0)
}
// killnameTable is the rip.c nlist[]: special death causes.
var killnameTable = []helpEntry{
{'a', "arrow", true},
{'b', "bolt", true},
{'d', "dart", true},
{'h', "hypothermia", false},
{'s', "starvation", false},
}
// killname converts a code to a monster name (rip.c killname).
func (g *RogueGame) killname(monst byte, doart bool) string {
var sp string
var article bool
if isUpper(monst) {
sp = g.Monsters[monst-'A'].Name
article = true
} else {
sp = "Wally the Wonder Badger"
article = false
for _, hp := range killnameTable {
if hp.Ch == monst {
sp = hp.Desc
article = hp.Print
break
}
}
}
if doart && article {
return "a" + vowelstr(sp) + " " + sp
}
return sp
}
// 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))]
}

196
game/score.go Normal file
View File

@@ -0,0 +1,196 @@
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
}
var scoreReasons = [4]string{
"killed",
"quit",
"A total winner",
"killed with Amulet",
}
// 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 f.Close()
var onDisk []ScoreEnt
if err := gob.NewDecoder(f).Decode(&onDisk); err != nil {
return topTen
}
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.
lock := g.ScorePath + ".lck"
for range 5 {
lf, err := os.OpenFile(lock, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
if err == nil {
lf.Close()
defer os.Remove(lock)
break
}
if fi, serr := os.Stat(lock); serr == nil &&
time.Since(fi.ModTime()) > 10*time.Second {
os.Remove(lock)
continue
}
time.Sleep(time.Second)
}
f, err := os.OpenFile(g.ScorePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
return
}
defer f.Close()
gob.NewEncoder(f).Encode(topTen)
}
// 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 {
uid := os.Getuid()
scp := len(topTen)
for i := range topTen {
if amount > topTen[i].Score {
scp = i
break
} else if !g.AllScore && flags != 2 &&
topTen[i].UID == uid && topTen[i].Flags != 2 {
// only one score per nowin uid
scp = len(topTen)
break
}
}
if scp < len(topTen) {
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(),
}
ins = scp
}
}
// Build the list display
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, scoreReasons[scp.Flags], scp.Level)
if scp.Flags == 0 || scp.Flags == 3 {
line += fmt.Sprintf(" by %s", g.killname(scp.Monster, true))
}
line += "."
if i == ins {
highlight = len(lines)
}
lines = append(lines, line)
}
if g.scr != nil && g.scr.term != nil {
g.clear()
for i, line := range lines {
if i == highlight {
g.standout()
}
g.mvaddstr(i, 0, line)
if i == highlight {
g.standend()
}
}
g.refresh()
} else {
for _, line := range lines {
fmt.Println(line)
}
}
// Update the list file
if ins >= 0 {
g.wrScore(topTen)
}
}
// 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)
}

View File

@@ -2,8 +2,322 @@ package game
import "fmt"
// sticks.c — wand and staff setup and naming. Zapping (do_zap, drain,
// fire_bolt) arrives with the combat phase.
// sticks.c — zap wands and staffs.
// doZap performs a zap with a wand (sticks.c do_zap).
func (g *RogueGame) doZap() {
p := &g.Player
obj := g.getItem("zap with", int(Stick))
if obj == nil {
return
}
if obj.Type != Stick {
g.After = false
g.msg("you can't zap with that!")
return
}
if obj.Charges() == 0 {
g.msg("nothing happens")
return
}
switch obj.Which {
case WsLight:
// Reddy Kilowatt wand. Light up the room
g.Items.Sticks[WsLight].Know = true
if p.Room.Flags.Has(IsGone) {
g.msg("the corridor glows and then fades")
} else {
p.Room.Flags.Clear(IsDark)
// Light the room and put the player back up
g.enterRoom(p.Pos)
g.addmsg("the room is lit")
if !g.Options.Terse {
g.addmsg(" by a shimmering %s light", g.pickColor("blue"))
}
g.endmsg()
}
case WsDrain:
// 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 p.Stats.HP < 2 {
g.msg("you are too weak to use it")
return
}
g.drain()
case WsInvis, WsPolymorph, WsTelAway, WsTelTo, WsCancel:
y := p.Pos.Y
x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y
x += g.Delta.X
}
if tp := g.Level.MonsterAt(y, x); tp != nil {
monster := tp.Type
if monster == 'F' {
p.Flags.Clear(IsHeld)
}
switch obj.Which {
case WsInvis:
tp.Flags.Set(IsInvis)
if g.cansee(y, x) {
g.mvaddch(y, x, tp.OldCh)
}
case WsPolymorph:
pp := tp.Pack
detachMon(&g.Level.Monsters, 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 = byte(g.rnd(26) + 'A')
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[WsPolymorph].Know = true
}
case WsCancel:
tp.Flags.Set(IsCanc)
tp.Flags.Clear(IsInvis | CanHuh)
tp.Disguise = tp.Type
if g.seeMonst(tp) {
g.mvaddch(y, x, tp.Disguise)
}
case WsTelAway, WsTelTo:
var newPos Coord
if obj.Which == WsTelAway {
for {
newPos, _ = g.findFloor(nil, 0, 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(IsRun)
g.relocate(tp, newPos)
}
}
case WsMissile:
g.Items.Sticks[WsMissile].Know = true
bolt := newObject()
bolt.Type = '*'
bolt.HurlDmg = "1x4"
bolt.HPlus = 100
bolt.DPlus = 1
bolt.Flags = IsMissl
if p.CurWeapon != nil {
bolt.Launch = 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")
} else {
g.msg("the missle vanishes with a puff of smoke")
}
case WsHasteM, WsSlowM:
y := p.Pos.Y
x := p.Pos.X
for stepOk(g.Level.VisibleChar(y, x)) {
y += g.Delta.Y
x += g.Delta.X
}
if tp := g.Level.MonsterAt(y, x); tp != nil {
if obj.Which == WsHasteM {
if tp.On(IsSlow) {
tp.Flags.Clear(IsSlow)
} else {
tp.Flags.Set(IsHaste)
}
} else {
if tp.On(IsHaste) {
tp.Flags.Clear(IsHaste)
} else {
tp.Flags.Set(IsSlow)
}
tp.Turn = true
}
g.Delta.Y = y
g.Delta.X = x
g.runto(g.Delta)
}
case WsElect, WsFire, WsCold:
var name string
switch obj.Which {
case WsElect:
name = "bolt"
case WsFire:
name = "flame"
default:
name = "ice"
}
g.fireBolt(p.Pos, &g.Delta, name)
g.Items.Sticks[obj.Which].Know = true
case WsNop:
}
obj.SetCharges(obj.Charges() - 1)
}
// 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)&FPNum]
}
inpass := p.Room.Flags.Has(IsGone)
var drainee []*Monster
for _, mp := range g.Level.Monsters {
if 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)&FPNum] == p.Room) {
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)
}
}
}
// 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.Type = Weapon
bolt.Which = Flame
bolt.HurlDmg = "6x6"
bolt.HPlus = 100
bolt.DPlus = 0
g.Items.Weapons[Flame].Name = name
var dirch byte
switch dir.Y + dir.X {
case 0:
dirch = '/'
case 1, -1:
if dir.Y == 0 {
dirch = '-'
} else {
dirch = '|'
}
case 2, -2:
dirch = '\\'
}
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)
bounce := false
switch ch {
case Door:
// this code is necessary if the hero is on a door and he
// fires at the wall the door is in, it would otherwise loop
// infinitely
if p.Pos != pos {
bounce = true
}
case '|', '-', ' ':
bounce = true
}
if bounce {
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
tp.OldCh = g.Level.Char(pos.Y, pos.X)
if !g.saveThrow(VsMagic, &tp.Stats) {
bolt.Pos = pos
used = true
if tp.Type == 'D' && name == "flame" {
g.addmsg("the flame bounces")
if !g.Options.Terse {
g.addmsg(" off the dragon")
}
g.endmsg()
} else {
g.hitMonster(pos, bolt)
}
} else 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))
}
}
} else if hitHero && pos == p.Pos {
hitHero = false
changed = !changed
if !g.save(VsMagic) {
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)
}
}
used = true
if g.Options.Terse {
g.msg("the %s hits", name)
} else {
g.msg("you are hit by the %s", name)
}
} else {
g.msg("the %s whizzes by you", name)
}
}
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))
}
}
// fixStick sets up a new wand or staff (sticks.c fix_stick).
func (g *RogueGame) fixStick(cur *Object) {

25
game/term_test.go Normal file
View File

@@ -0,0 +1,25 @@
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
}
func (t *testTerm) Render(*Window) {}
func (t *testTerm) ReadChar() byte {
if t.pos < len(t.input) {
c := t.input[t.pos]
t.pos++
return c
}
t.tick++
if t.tick%2 == 0 {
return '\n'
}
return ' '
}

View File

@@ -2,11 +2,125 @@ package game
import "fmt"
// weapons.c — weapon initialization and naming. The throwing half
// (missile, do_motion, hit_monster) arrives with the combat phase.
// weapons.c — functions for dealing with problems brought about by weapons.
const noWeapon = -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 := g.getItem("throw", int(Weapon))
if obj == nil {
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 {
// Erase the old one
if obj.Pos != p.Pos && g.cansee(obj.Pos.Y, obj.Pos.X) && !g.Options.Terse {
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)
}
// 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 {
// 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.Type)
g.refresh()
}
continue
}
break
}
}
// 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.Type
obj.Pos = fpos
if g.cansee(fpos.Y, fpos.X) {
if pp.Monst != nil {
pp.Monst.OldCh = obj.Type
} else {
g.mvaddch(fpos.Y, fpos.X, obj.Type)
}
}
attachObj(&g.Level.Objects, 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 := g.getItem("wield", int(Weapon))
if obj == nil {
g.After = false
return
}
if obj.Type == Armor {
g.msg("you can't wield armor")
g.After = false
return
}
if g.isCurrent(obj) {
g.After = false
return
}
sp := g.invName(obj, true)
p.CurWeapon = obj
if !g.Options.Terse {
g.addmsg("you are now ")
}
g.msg("wielding %s (%c)", sp, obj.PackCh)
}
// initWeaps is the weapons.c init_dam[] table.
var initWeaps = [MaxWeapons]struct {
dam string // damage when wielded

111
game/wizard.go Normal file
View File

@@ -0,0 +1,111 @@
package game
// wizard.c — special wizard commands, some of which are also non-wizard
// commands under strange circumstances. The MASTER-only debug commands
// (create_obj, show_map) arrive with the UI phase.
// whatis identifies what a certain object is (wizard.c whatis).
func (g *RogueGame) whatis(insist bool, typ int) {
p := &g.Player
if len(p.Pack) == 0 {
g.msg("you don't have anything in your pack to identify")
return
}
var obj *Object
for {
obj = g.getItem("identify", typ)
if !insist {
break
}
if g.NObjs == 0 {
return
}
if obj == nil {
g.msg("you must identify something")
} else if typ != 0 && int(obj.Type) != typ &&
!(typ == RorS && (obj.Type == Ring || obj.Type == Stick)) {
g.msg("you must identify a %s", typeName(typ))
} else {
break
}
}
if obj == nil {
return
}
switch obj.Type {
case Scroll:
setKnow(obj, g.Items.Scrolls[:])
case Potion:
setKnow(obj, g.Items.Potions[:])
case Stick:
setKnow(obj, g.Items.Sticks[:])
case Weapon, Armor:
obj.Flags.Set(IsKnow)
case Ring:
setKnow(obj, g.Items.Rings[:])
}
g.msg("%s", g.invName(obj, false))
}
// 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(IsKnow)
info[obj.Which].Guess = ""
}
// typeNameTable is the wizard.c type_name static tlist.
var typeNameTable = []struct {
ch int
desc string
}{
{int(Potion), "potion"},
{int(Scroll), "scroll"},
{int(Food), "food"},
{RorS, "ring, wand or staff"},
{int(Ring), "ring"},
{int(Stick), "wand or staff"},
{int(Weapon), "weapon"},
{int(Armor), "suit of armor"},
}
// typeName returns the name of an object type (wizard.c type_name).
func typeName(typ int) string {
for _, hp := range typeNameTable {
if typ == hp.ch {
return hp.desc
}
}
return ""
}
// 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(nil, 0, 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(IsHeld) {
p.Flags.Clear(IsHeld)
p.VfHit = 0
g.Monsters['F'-'A'].Stats.Dmg = "000x0"
}
g.NoMove = 0
g.Count = 0
g.Running = false
g.flushType()
}