diff --git a/game/armor.go b/game/armor.go new file mode 100644 index 0000000..b8d4d26 --- /dev/null +++ b/game/armor.go @@ -0,0 +1,66 @@ +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 := g.getItem("wear", int(Armor)) + if obj == nil { + return + } + if p.CurArmor != nil { + g.addmsg("you are already wearing some") + if !g.Options.Terse { + g.addmsg(". You'll have to take it off first") + } + g.endmsg() + g.After = false + return + } + if obj.Type != Armor { + g.msg("you can't wear that") + return + } + g.wasteTime() + obj.Flags.Set(IsKnow) + sp := g.invName(obj, true) + p.CurArmor = obj + if !g.Options.Terse { + g.addmsg("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.addmsg("was") + } else { + g.addmsg("you used to be") + } + g.msg(" wearing %c) %s", obj.PackCh, g.invName(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) +} diff --git a/game/chase.go b/game/chase.go new file mode 100644 index 0000000..cfbb5cd --- /dev/null +++ b/game/chase.go @@ -0,0 +1,138 @@ +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. + +// setOldch sets the oldch character for the monster (chase.c set_oldch). +func (g *RogueGame) setOldch(tp *Monster, cp Coord) { + if tp.Pos == cp { + return + } + sch := tp.OldCh + tp.OldCh = g.mvinch(cp.Y, cp.X) + if !g.Player.On(IsBlind) { + if (sch == Floor || tp.OldCh == Floor) && tp.Room.Flags.Has(IsDark) { + 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(IsBlind) { + return false + } + if mp.On(IsInvis) && !p.On(CanSee) { + 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(IsDark) +} + +// 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(IsRun) + tp.Flags.Clear(IsHeld) + 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(FPass) { + return &g.Level.Passages[fp&FPNum] + } + + 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(IsBlind) { + return false + } + if distance(y, x, p.Pos.Y, p.Pos.X) < LampDist { + if g.Level.FlagsAt(y, x).Has(FPass) { + 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(IsDark) +} + +// 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 + if prob <= 0 || tp.Room == g.Player.Room || g.seeMonst(tp) { + return &g.Player.Pos + } + for _, obj := range g.Level.Objects { + if obj.Type == Scroll && obj.Which == SScare { + continue + } + if g.roomin(obj.Pos) == tp.Room && g.rnd(100) < prob { + claimed := false + for _, other := range g.Level.Monsters { + if other.Dest == &obj.Pos { + claimed = true + break + } + } + if !claimed { + return &obj.Pos + } + } + } + return &g.Player.Pos +} diff --git a/game/daemons.go b/game/daemons.go index 6201e83..c5dda1c 100644 --- a/game/daemons.go +++ b/game/daemons.go @@ -1,17 +1,205 @@ package game -// daemons.c — the daemon and fuse callbacks. Each callback is ported in the -// phase that owns its subsystem; runDaemon is the dispatch switch that -// replaces the C function pointers. +// 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) { - _ = arg switch id { + case DRollwand: + g.rollwand(arg) + case DDoctor: + g.doctor(arg) + case DSwander: + g.swander(arg) + case DNohaste: + g.nohaste(arg) + case DUnconfuse: + g.unconfuse(arg) + case DUnsee: + g.unsee(arg) + case DSight: + g.sight(arg) + case DVisuals: + g.visuals(arg) + case DComeDown: + g.comeDown(arg) + case DLand: + g.land(arg) default: // Callbacks are added to this switch as their subsystems are - // ported; reaching one that isn't here is a porting bug, not a - // game state. + // ported; reaching one that isn't here is a porting bug. panic("daemon not yet ported") } } + +// 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, RRegen) { + p.Stats.HP++ + } + if p.IsRing(Right, RRegen) { + 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(IsHuh) + 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(IsInvis) && g.seeMonst(th) { + g.mvaddch(th.Pos.Y, th.Pos.X, th.OldCh) + } + } + g.Player.Flags.Clear(CanSee) +} + +// sight gives the hero his sight back (daemons.c sight). +func (g *RogueGame) sight(int) { + p := &g.Player + if p.On(IsBlind) { + g.Extinguish(DSight) + p.Flags.Clear(IsBlind) + if !p.Room.Flags.Has(IsGone) { + 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(IsHaste) + g.msg("you feel yourself slowing down") +} + +// comeDown takes the hero down off her acid trip (daemons.c come_down). +func (g *RogueGame) comeDown(int) { + p := &g.Player + if !p.On(IsHalu) { + return + } + + g.KillDaemon(DVisuals) + p.Flags.Clear(IsHalu) + + if p.On(IsBlind) { + 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.Type) + } + } + + // undo the monsters + seemonst := p.On(SeeMonst) + 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(IsInvis) || p.On(CanSee) { + 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) { + p := &g.Player + 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 + seemonst := p.On(SeeMonst) + 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(byte(g.rnd(26) + 'A')) + } + } else if seemonst { + g.standout() + g.addch(byte(g.rnd(26) + 'A')) + g.standend() + } + } +} + +// land lands the hero from a levitation potion (daemons.c land). +func (g *RogueGame) land(int) { + g.Player.Flags.Clear(IsLevit) + g.msg("%s", g.chooseStr("bummer! You've hit the ground", + "you float gently to the ground")) +} diff --git a/game/fight.go b/game/fight.go new file mode 100644 index 0000000..452d8a3 --- /dev/null +++ b/game/fight.go @@ -0,0 +1,28 @@ +package game + +// fight.c — combat. setMname arrives first (monster wake-ups need it); the +// combat resolution functions come with the combat phase. + +// 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(SeeMonst) { + if g.Options.Terse { + return "it" + } + return "something" + } + var mname string + if g.Player.On(IsHalu) { + ch := int(g.mvinch(tp.Pos.Y, tp.Pos.X)) + if !isUpper(byte(ch)) { + ch = g.rnd(26) + } else { + ch -= 'A' + } + mname = monsterTable[ch].Name + } else { + mname = monsterTable[tp.Type-'A'].Name + } + return "the " + mname +} diff --git a/game/game.go b/game/game.go index d34bee5..1a6b9a1 100644 --- a/game/game.go +++ b/game/game.go @@ -33,12 +33,13 @@ type Options struct { // Config carries everything needed to construct a game. type Config struct { - Seed int32 // dungeon number; the caller derives it (time+pid or SEED env) - 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) + Seed int32 // dungeon number; the caller derives it (time+pid or SEED env) + 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 @@ -92,11 +93,23 @@ type RogueGame struct { LLastDir byte LastPick *Object LLastPick *Object + lastDelt Coord // misc.c get_dir static last_delt + + // dungeon generation working state + maze mazeState // rooms.c maze statics + pnum int // passages.c passnum statics + newpnum bool // daemons/fuses Daemons DaemonList - // UI state (the Screen itself arrives with the UI phase) + // screen / messages + scr *Screen + Msgs MsgLine + 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? @@ -138,18 +151,58 @@ func NewGame(cfg Config) *RogueGame { InvType: InvOver, } g.InvDescribe = true + g.Msgs.SaveMsg = true + g.scr = NewScreen(cfg.Term) g.FileName = cfg.Home + "/rogue.save" if cfg.Wizard { g.Player.Flags.Set(SeeMonst) } // TODO(port): parse cfg.RogueOpts once options.go lands. - g.initProbs() // set up prob tables for objects - // TODO(port): initPlayer() goes exactly here (RNG order!) once pack.go lands. + g.Items.Group = 2 // weapons.c: int group = 2 + for i := range g.Level.Passages { + g.Level.Passages[i].Flags = IsGone | IsDark + } + + 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 - // TODO(port): new level + daemon start-up once their phases land. return g } + +// 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.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() + 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 +} + +// 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 +} diff --git a/game/init.go b/game/init.go index ee51688..e4d7b0c 100644 --- a/game/init.go +++ b/game/init.go @@ -3,7 +3,49 @@ package game import "strings" // init.c — per-game randomization of item appearances and probability -// tables. initPlayer arrives with the pack/items phase (it needs addPack). +// tables, and the player roll-up. + +// initPlayer rolls her up (init.c init_player). +func (g *RogueGame) initPlayer() { + p := &g.Player + p.MaxStats = initStats + p.Stats = p.MaxStats + p.FoodLeft = HungerTime + // Give him some food + obj := newObject() + obj.Type = Food + obj.Count = 1 + g.addPack(obj, true) + // And his suit of armor + obj = newObject() + obj.Type = Armor + obj.Which = RingMail + obj.Arm = aClass[RingMail] - 1 + obj.Flags.Set(IsKnow) + obj.Count = 1 + p.CurArmor = obj + g.addPack(obj, true) + // Give him his weaponry. First a mace. + obj = newObject() + g.initWeapon(obj, Mace) + obj.HPlus = 1 + obj.DPlus = 1 + obj.Flags.Set(IsKnow) + g.addPack(obj, true) + p.CurWeapon = obj + // Now a +1 bow + obj = newObject() + g.initWeapon(obj, Bow) + obj.HPlus = 1 + obj.Flags.Set(IsKnow) + g.addPack(obj, true) + // Now some arrows + obj = newObject() + g.initWeapon(obj, Arrow) + obj.Count = g.rnd(15) + 25 + obj.Flags.Set(IsKnow) + g.addPack(obj, true) +} // initColors initializes the potion color scheme for this game // (init.c init_colors). diff --git a/game/io.go b/game/io.go new file mode 100644 index 0000000..5fea9e4 --- /dev/null +++ b/game/io.go @@ -0,0 +1,231 @@ +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 + +// MsgLine is the io.c message machinery: the static msgbuf/newpos pair plus +// the related globals (mpos, huh, and the message-behavior flags). +type MsgLine 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-- +} + +// 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 (g *RogueGame) msg(format string, a ...any) int { + // if the string is "", just clear the line + if format == "" { + g.move(0, 0) + g.clrtoeol() + g.Msgs.Mpos = 0 + return ^Escape + } + // otherwise add to the message and flush it out + g.doadd(format, a...) + return g.endmsg() +} + +// addmsg adds things to the current message (io.c addmsg). +func (g *RogueGame) addmsg(format string, a ...any) { + g.doadd(format, a...) +} + +// endmsg 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 (g *RogueGame) endmsg() int { + m := &g.Msgs + if m.SaveMsg { + m.Huh = m.buf.String() + } + if m.Mpos != 0 { + g.look(false) + g.mvaddstr(0, m.Mpos, "--More--") + g.refresh() + if !m.MsgEsc { + g.waitFor(' ') + } else { + for { + ch := g.readchar() + if ch == ' ' { + break + } + if ch == Escape { + m.buf.Reset() + m.Mpos = 0 + m.newpos = 0 + 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:] + } + g.mvaddstr(0, 0, out) + g.clrtoeol() + m.Mpos = m.newpos + m.newpos = 0 + m.buf.Reset() + g.refresh() + return ^Escape +} + +// doadd performs an add onto the message buffer (io.c doadd). +func (g *RogueGame) doadd(format string, a ...any) { + m := &g.Msgs + s := fmt.Sprintf(format, a...) + if len(s)+m.newpos >= maxMsg { + g.endmsg() + } + m.buf.WriteString(s) + m.newpos = m.buf.Len() +} + +// 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). +func (g *RogueGame) readchar() byte { + ch := g.scr.term.ReadChar() + 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 +} + +var hungerStateName = [...]string{"", "Hungry", "Weak", "Faint"} + +// 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.Arm + if p.CurArmor != nil { + temp = p.CurArmor.Arm + } + if 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 { + 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, + 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) +} + +// 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 g.readchar() != ch { + } +} + +// 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 ; 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 +} diff --git a/game/misc.go b/game/misc.go new file mode 100644 index 0000000..e04d738 --- /dev/null +++ b/game/misc.go @@ -0,0 +1,455 @@ +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). + +// look takes a quick glance all around the player (misc.c look). +func (g *RogueGame) look(wakeup bool) { + p := &g.Player + hero := p.Pos + passcount := 0 + rp := p.Room + if g.Oldpos != hero { + g.eraseLamp(g.Oldpos, g.Oldrp) + g.Oldpos = hero + g.Oldrp = rp + } + ey := hero.Y + 1 + ex := hero.X + 1 + sx := hero.X - 1 + sy := hero.Y - 1 + sumhero, diffhero := 0, 0 + if g.DoorStop && !g.Firstmove && g.Running { + sumhero = hero.Y + hero.X + diffhero = hero.Y - hero.X + } + pp := g.Level.At(hero.Y, hero.X) + pch := pp.Ch + pfl := pp.Flags + + for y := sy; y <= ey; y++ { + if y <= 0 || y >= NumLines-1 { + continue + } + for x := sx; x <= ex; x++ { + if x < 0 || x >= NumCols { + continue + } + if !p.On(IsBlind) { + if y == hero.Y && x == hero.X { + continue + } + } + + pp := g.Level.At(y, x) + ch := pp.Ch + if ch == ' ' { // nothing need be done with a ' ' + continue + } + fp := &pp.Flags + if pch != Door && ch != Door { + if (pfl & FPass) != (*fp & FPass) { + continue + } + } + if (fp.Has(FPass) || ch == Door) && (pfl.Has(FPass) || pch == Door) { + if hero.X != x && hero.Y != y && + !stepOk(g.Level.Char(y, hero.X)) && !stepOk(g.Level.Char(hero.Y, x)) { + continue + } + } + + tp := pp.Monst + if tp == nil { + ch = g.tripCh(y, x, ch) + } else if p.On(SeeMonst) && tp.On(IsInvis) { + if g.DoorStop && !g.Firstmove { + g.Running = false + } + continue + } else { + if wakeup { + g.wakeMonster(y, x) + } + if g.seeMonst(tp) { + if p.On(IsHalu) { + ch = byte(g.rnd(26) + 'A') + } else { + ch = tp.Disguise + } + } + } + if p.On(IsBlind) && (y != hero.Y || x != hero.X) { + continue + } + + g.move(y, x) + + if p.Room.Flags.Has(IsDark) && !g.Options.SeeFloor && ch == Floor { + ch = ' ' + } + + if tp != nil || ch != g.inch() { + g.addch(ch) + } + + if g.DoorStop && !g.Firstmove && g.Running { + switch g.RunCh { + case 'h': + if x == ex { + continue + } + case 'j': + if y == sy { + continue + } + case 'k': + if y == ey { + continue + } + case 'l': + if x == sx { + continue + } + case 'y': + if (y+x)-sumhero >= 1 { + continue + } + case 'u': + if (y-x)-diffhero >= 1 { + continue + } + case 'n': + if (y+x)-sumhero <= -1 { + continue + } + case 'b': + if (y-x)-diffhero <= -1 { + continue + } + } + switch ch { + case Door: + if x == hero.X || y == hero.Y { + g.Running = false + } + case Passage: + if x == hero.X || y == hero.Y { + passcount++ + } + case Floor, '|', '-', ' ': + default: + g.Running = false + } + } + } + } + if g.DoorStop && !g.Firstmove && passcount > 1 { + g.Running = false + } + if !g.Running || !g.Options.Jump { + g.mvaddch(hero.Y, hero.X, PlayerCh) + } +} + +// 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(IsHalu) && 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&(IsGone|IsDark) == IsDark && + !g.Player.On(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 == 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&(IsGone|IsDark) == IsDark && !g.Player.On(IsBlind) { + return g.Options.SeeFloor + } + return true +} + +// findObj finds the unclaimed object at (y, x) (misc.c find_obj). +func (g *RogueGame) findObj(y, x int) *Object { + for _, obj := range g.Level.Objects { + if obj.Pos.Y == y && obj.Pos.X == x { + return obj + } + } + return nil +} + +// eat lets her try to eat something (misc.c eat). +func (g *RogueGame) eat() { + obj := g.getItem("eat", int(Food)) + if obj == nil { + return + } + if obj.Type != Food { + 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 + } + if obj.Which == 1 { + g.msg("my, that was a yummy %s", g.Fruit) + } else if g.rnd(100) > 70 { + p.Stats.Exp++ + g.msg("%s, this food tastes awful", g.chooseStr("bummer", "yuk")) + g.checkLevel() + } else { + 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; eLevels[i] != 0; i++ { + if 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) + } +} + +// chgStr modifies the player's strength, keeping track of the highest it +// has been (misc.c chg_str). +func (g *RogueGame) chgStr(amt int) { + if amt == 0 { + return + } + p := &g.Player + addStr(&p.Stats.Str, amt) + comp := p.Stats.Str + if p.IsRing(Left, RAddStr) { + addStr(&comp, -p.CurRing[Left].Arm) + } + if p.IsRing(Right, RAddStr) { + addStr(&comp, -p.CurRing[Right].Arm) + } + 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(IsHaste) { + g.NoCommand += g.rnd(8) + p.Flags.Clear(IsRun | IsHaste) + g.Extinguish(DNohaste) + g.msg("you faint from exhaustion") + return false + } + p.Flags.Set(IsHaste) + 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.addmsg("That's already ") + } + g.msg("in use") + return true + } + return false +} + +// getDir sets up the direction coordinate for use in various "prefix" +// commands (misc.c get_dir). +func (g *RogueGame) getDir() 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 { + gotit := true + switch g.DirCh = g.readchar(); g.DirCh { + case 'h', 'H': + g.Delta = Coord{X: -1, Y: 0} + case 'j', 'J': + g.Delta = Coord{X: 0, Y: 1} + case 'k', 'K': + g.Delta = Coord{X: 0, Y: -1} + case 'l', 'L': + g.Delta = Coord{X: 1, Y: 0} + case 'y', 'Y': + g.Delta = Coord{X: -1, Y: -1} + case 'u', 'U': + g.Delta = Coord{X: 1, Y: -1} + case 'b', 'B': + g.Delta = Coord{X: -1, Y: 1} + case 'n', 'N': + g.Delta = Coord{X: 1, Y: 1} + case Escape: + g.LastDir = 0 + g.resetLast() + return false + default: + g.Msgs.Mpos = 0 + g.msg("%s", prompt) + gotit = false + } + if gotit { + break + } + } + g.DirCh = toLower(g.DirCh) + g.LastDir = g.DirCh + g.lastDelt = g.Delta + } + if g.Player.On(IsHuh) && g.rnd(5) == 0 { + for { + g.Delta.Y = g.rnd(3) - 1 + g.Delta.X = g.rnd(3) - 1 + if g.Delta.Y != 0 || g.Delta.X != 0 { + break + } + } + } + g.Msgs.Mpos = 0 + return true +} + +// thingList is misc.c rnd_thing()'s static table. +var thingList = []byte{ + Potion, Scroll, Ring, Stick, Food, Weapon, Armor, Stairs, Gold, Amulet, +} + +// 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(thingList)) + } else { + i = g.rnd(len(thingList) - 1) + } + return 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(IsHalu) { + 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) +} diff --git a/game/monsters.go b/game/monsters.go new file mode 100644 index 0000000..8204d11 --- /dev/null +++ b/game/monsters.go @@ -0,0 +1,193 @@ +package game + +// monsters.c — monster creation and saving throws. + +// lvlMons and wandMons list monsters in rough order of vorpalness; zero +// entries in wandMons never wander (monsters.c). +var 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', +} + +var 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, +} + +// 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 := &lvlMons + if wander { + mons = &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 := g.Depth - AmuletLevel + if levAdd < 0 { + levAdd = 0 + } + attachMon(&g.Level.Monsters, 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 := &monsterTable[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.Arm = mp.Stats.Arm - 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(IsHaste) + } + tp.Turn = true + tp.Pack = nil + if g.Player.IsWearing(RAggr) { + 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(nil, 0, true) + if g.roomin(cp) != g.Player.Room { + break + } + } + g.newMonster(tp, g.randMonster(true), cp) + if g.Player.On(SeeMonst) { + g.standout() + if !g.Player.On(IsHalu) { + g.addch(tp.Type) + } else { + g.addch(byte(g.rnd(26) + 'A')) + } + 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) *Monster { + p := &g.Player + tp := g.Level.MonsterAt(y, x) + if tp == nil { + panic("can't find monster in wake_monster") + } + ch := tp.Type + // Every time he sees a mean monster, it might start chasing him + if !tp.On(IsRun) && g.rnd(3) != 0 && tp.On(IsMean) && !tp.On(IsHeld) && + !p.IsWearing(RStealth) && !p.On(IsLevit) { + tp.Dest = &p.Pos + tp.Flags.Set(IsRun) + } + if ch == 'M' && !p.On(IsBlind) && !p.On(IsHalu) && + !tp.On(IsFound) && !tp.On(IsCanc) && tp.On(IsRun) { + rp := p.Room + if (rp != nil && !rp.Flags.Has(IsDark)) || + distance(y, x, p.Pos.Y, p.Pos.X) < LampDist { + tp.Flags.Set(IsFound) + if !g.save(VsMagic) { + if p.On(IsHuh) { + g.Lengthen(DUnconfuse, g.spread(HuhDuration)) + } else { + g.Fuse(DUnconfuse, 0, g.spread(HuhDuration), After) + } + p.Flags.Set(IsHuh) + mname := g.setMname(tp) + g.addmsg("%s", mname) + if mname != "it" { + g.addmsg("'") + } + g.msg("s gaze has confused you") + } + } + } + // Let greedy ones guard gold + if tp.On(IsGreed) && !tp.On(IsRun) { + tp.Flags.Set(IsRun) + if p.Room.GoldVal != 0 { + tp.Dest = &p.Room.Gold + } else { + tp.Dest = &p.Pos + } + } + return tp +} + +// 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 { + 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, RProtect) { + which -= p.CurRing[Left].Arm + } + if p.IsRing(Right, RProtect) { + which -= p.CurRing[Right].Arm + } + } + return g.saveThrow(which, &p.Stats) +} diff --git a/game/move.go b/game/move.go new file mode 100644 index 0000000..7180798 --- /dev/null +++ b/game/move.go @@ -0,0 +1,19 @@ +package game + +// move.c — hero movement. doorOpen arrives first (room entry needs it); +// the movement commands and traps come with the UI phase. + +// 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(IsGone) { + 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) + } + } + } +} diff --git a/game/newlevel.go b/game/newlevel.go new file mode 100644 index 0000000..062f300 --- /dev/null +++ b/game/newlevel.go @@ -0,0 +1,159 @@ +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(IsHeld) // 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.doRooms() // Draw rooms + g.doPassages() // Draw passages + p.NoFood++ + g.putThings() // Place objects (if any) + // Place the traps + if g.rnd(10) < g.Depth { + g.Level.NTraps = g.rnd(g.Depth/4) + 1 + if g.Level.NTraps > MaxTraps { + g.Level.NTraps = MaxTraps + } + for i := g.Level.NTraps; 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(nil, 0, 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(NTraps)) + } + } + // Place the staircase down. + stairs, _ := g.findFloor(nil, 0, 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(nil, 0, true) + p.Pos = hero + g.enterRoom(hero) + g.mvaddch(hero.Y, hero.X, PlayerCh) + if p.On(SeeMonst) { + g.turnSee(false) + } + if p.On(IsHalu) { + g.visuals(0) + } +} + +// rndRoom picks a room that is really there (new_level.c rnd_room). +func (g *RogueGame) rndRoom() int { + for { + rm := g.rnd(MaxRooms) + if !g.Level.Rooms[rm].Flags.Has(IsGone) { + 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.treasRoom() + } + // Do MAXOBJ attempts to put things on a level + for i := 0; i < MaxObj; i++ { + if g.rnd(100) < 36 { + // Pick a new object and link it in the list + obj := g.newThing() + attachObj(&g.Level.Objects, obj) + // Put it somewhere + obj.Pos, _ = g.findFloor(nil, 0, false) + g.Level.SetChar(obj.Pos.Y, obj.Pos.X, obj.Type) + } + } + // 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() + attachObj(&g.Level.Objects, obj) + obj.Damage = "0x0" + obj.HurlDmg = "0x0" + obj.Arm = 11 + obj.Type = Amulet + // Put it somewhere + obj.Pos, _ = g.findFloor(nil, 0, false) + g.Level.SetChar(obj.Pos.Y, obj.Pos.X, Amulet) + } +} + +// treasRoom adds a treasure room (new_level.c treas_room). +func (g *RogueGame) treasRoom() { + rp := &g.Level.Rooms[g.rndRoom()] + spots := (rp.Max.Y-2)*(rp.Max.X-2) - minTreas + if spots > maxTreas-minTreas { + spots = 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 + attachObj(&g.Level.Objects, tp) + g.Level.SetChar(mp.Y, mp.X, tp.Type) + } + + // fill up room with monsters from the next level down + nm := g.rnd(spots) + minTreas + if nm < numMonst+2 { + nm = 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(IsMean) // no sloughers in THIS room + g.givePack(tp) + } + } + g.Depth-- +} diff --git a/game/newlevel_test.go b/game/newlevel_test.go new file mode 100644 index 0000000..da0c962 --- /dev/null +++ b/game/newlevel_test.go @@ -0,0 +1,124 @@ +package game + +import ( + "strings" + "testing" +) + +func genLevel(t *testing.T, seed int32) *RogueGame { + t.Helper() + g := NewGame(Config{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 := 0; y < NumLines; y++ { + for x := 0; x < NumCols; x++ { + 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) { + for _, seed := range []int32{1, 12345, 2026, 99999} { + g := genLevel(t, seed) + + // 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) + } + + // Some rooms exist and are drawn. + 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) + } + + // Every monster is indexed on the map and placed in a room. + 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) + } + } + + // Every level object sits on a cell displaying its type (items can + // share cells only with monsters standing on them). + for _, obj := range g.Level.Objects { + ch := g.Level.Char(obj.Pos.Y, obj.Pos.X) + if ch != obj.Type && g.Level.MonsterAt(obj.Pos.Y, obj.Pos.X) == nil { + t.Errorf("seed %d: object %q at (%d,%d) but map shows %q", + seed, obj.Type, obj.Pos.Y, obj.Pos.X, ch) + } + } + + // The player has her starting kit: food, armor, mace, bow, arrows. + 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.Which != Mace { + t.Errorf("seed %d: not wielding the starting mace", seed) + } + if g.Player.CurArmor == nil || g.Player.CurArmor.Which != RingMail { + t.Errorf("seed %d: not wearing the starting ring mail", seed) + } + } +} + +func TestNewLevelDeterministic(t *testing.T) { + 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) { + for _, seed := range []int32{7, 42, 1000, 31337} { + g := NewGame(Config{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) + } + } + } +} diff --git a/game/pack.go b/game/pack.go new file mode 100644 index 0000000..552ef1c --- /dev/null +++ b/game/pack.go @@ -0,0 +1,400 @@ +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.findObj(p.Pos.Y, p.Pos.X); obj == nil { + return + } + fromFloor = true + } + + // Check for and deal with scare monster scrolls + if obj.Type == Scroll && obj.Which == SScare && obj.Flags.Has(ObjIsFound) { + detachObj(&g.Level.Objects, obj) + g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh()) + if p.Room.Flags.Has(IsGone) { + 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 + } + + if len(p.Pack) == 0 { + p.Pack = append(p.Pack, obj) + obj.PackCh = g.packChar() + p.Inpack++ + } else { + // Walk the pack looking for the insertion point, keeping items of + // one type together and merging stackable/grouped items — a direct + // translation of the C linked-list walk. lp is the index to insert + // after; -1 after a merge means no insertion. + lp := -1 + merged := false + for i := 0; i < len(p.Pack); i++ { + if p.Pack[i].Type != obj.Type { + lp = i + continue + } + // found the group of our type: scan for matching subtype + for p.Pack[i].Type == obj.Type && p.Pack[i].Which != obj.Which { + lp = i + if i+1 >= len(p.Pack) { + break + } + i++ + } + op := p.Pack[i] + if op.Type == obj.Type && op.Which == obj.Which { + if IsMult(op.Type) { + if !g.packRoom(fromFloor, obj) { + return + } + op.Count++ + obj = op + lp = -1 + merged = true + } else if obj.Group != 0 { + lp = i + for p.Pack[i].Type == obj.Type && + 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.Type == obj.Type && op.Which == obj.Which && + op.Group == obj.Group { + op.Count += obj.Count + p.Inpack-- + if !g.packRoom(fromFloor, obj) { + return + } + obj = op + lp = -1 + merged = true + } + } else { + lp = i + } + } + break + } + + if !merged && lp != -1 { + if !g.packRoom(fromFloor, obj) { + return + } + obj.PackCh = g.packChar() + p.Pack = append(p.Pack[:lp+1], + append([]*Object{obj}, p.Pack[lp+1:]...)...) + } + } + + obj.Flags.Set(ObjIsFound) + + // 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.Type == Amulet { + g.HasAmulet = true + } + // Notify the user + if !silent { + if !g.Options.Terse { + g.addmsg("you now have ") + } + g.msg("%s (%c)", g.invName(obj, !g.Options.Terse), obj.PackCh) + } +} + +// 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.addmsg("there's ") + } + g.addmsg("no room") + if !g.Options.Terse { + g.addmsg(" in your pack") + } + g.endmsg() + if fromFloor { + g.moveMsg(obj) + } + p.Inpack = MaxPack + return false + } + + if fromFloor { + detachObj(&g.Level.Objects, obj) + g.mvaddch(p.Pos.Y, p.Pos.X, g.floorCh()) + if p.Room.Flags.Has(IsGone) { + 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). +func (g *RogueGame) leavePack(obj *Object, newobj, all bool) *Object { + p := &g.Player + p.Inpack-- + nobj := obj + if obj.Count > 1 && !all { + g.LastPick = obj + obj.Count-- + if obj.Group != 0 { + p.Inpack++ + } + if newobj { + copied := *obj + nobj = &copied + nobj.Count = 1 + } + } else { + g.LastPick = nil + p.PackUsed[obj.PackCh-'a'] = false + detachObj(&p.Pack, obj) + } + return nobj +} + +// packChar returns the next unused pack character (pack.c pack_char). +func (g *RogueGame) packChar() byte { + p := &g.Player + 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 +} + +// 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, typ int) bool { + g.NObjs = 0 + for _, item := range list { + if typ != 0 && typ != int(item.Type) && + !(typ == Callable && item.Type != Food && item.Type != Amulet) && + !(typ == RorS && (item.Type == Ring || item.Type == Stick)) { + continue + } + g.NObjs++ + g.Msgs.MsgEsc = true + line := string(item.PackCh) + ") " + g.invName(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 g.Options.Terse { + if typ == 0 { + g.msg("empty handed") + } else { + g.msg("nothing appropriate") + } + } else { + if typ == 0 { + g.msg("you are empty handed") + } else { + g.msg("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(IsLevit) { + return + } + + obj := g.findObj(p.Pos.Y, p.Pos.X) + if g.MoveOn { + g.moveMsg(obj) + return + } + switch ch { + case Gold: + if obj == nil { + return + } + g.money(obj.GoldVal()) + detachObj(&g.Level.Objects, obj) + p.Room.GoldVal = 0 + default: + g.addPack(nil, false) + } +} + +// 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.addmsg("you ") + } + g.msg("moved onto %s", g.invName(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.invName(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.invName(obj, false)) + return + } + } + g.msg("'%s' not in pack", unctrl(mch)) +} + +// getItem picks something out of a pack for a purpose (pack.c get_item). +func (g *RogueGame) getItem(purpose string, typ int) *Object { + p := &g.Player + if len(p.Pack) == 0 { + g.msg("you aren't carrying anything") + return nil + } + if g.Again { + if g.LastPick != nil { + return g.LastPick + } + g.msg("you ran out") + return nil + } + for { + if !g.Options.Terse { + g.addmsg("which object do you want to ") + } + g.addmsg("%s", purpose) + if g.Options.Terse { + g.addmsg(" what") + } + g.msg("? (* for list): ") + 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 + } + g.NObjs = 1 // normal case: person types one char + if ch == '*' { + g.Msgs.Mpos = 0 + if !g.inventory(p.Pack, typ) { + g.After = false + return nil + } + continue + } + for _, obj := range p.Pack { + if obj.PackCh == ch { + return obj + } + } + g.msg("'%s' is not a valid item", unctrl(ch)) + } +} + +// 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(IsGone) { + 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.addmsg("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(IsGone) { + 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 +} diff --git a/game/passages.go b/game/passages.go new file mode 100644 index 0000000..a7604bb --- /dev/null +++ b/game/passages.go @@ -0,0 +1,302 @@ +package game + +// passages.c — draw the connecting passages. + +// rdesConn is the hardcoded 3x3 room adjacency matrix from do_passages. +var 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}, +} + +// doPassages draws all the passages on a level (passages.c do_passages). +func (g *RogueGame) doPassages() { + var isconn [MaxRooms][MaxRooms]bool + var 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 { + // find a room to connect with + j := 0 + r2 := -1 + for i := 0; i < MaxRooms; i++ { + if rdesConn[r1][i] && !ingraph[i] { + if j++; g.rnd(j) == 0 { + r2 = i + } + } + } + if j == 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 + } + } + } else { + // otherwise, connect new room to the graph, and draw a tunnel + // to it + ingraph[r2] = true + g.conn(r1, r2) + isconn[r1][r2] = true + isconn[r2][r1] = true + roomcount++ + } + if roomcount >= MaxRooms { + break + } + } + + // 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 + j := 0 + r2 := -1 + for i := 0; i < MaxRooms; i++ { + if rdesConn[r1][i] && !isconn[r1][i] { + if j++; g.rnd(j) == 0 { + r2 = i + } + } + } + // if there is one, connect it and look for the next added passage + if j != 0 { + g.conn(r1, r2) + isconn[r1][r2] = true + isconn[r2][r1] = true + } + } + g.passnum() +} + +// conn draws a corridor from a room in a certain direction (passages.c +// conn). +func (g *RogueGame) conn(r1, r2 int) { + var rm int + var direc byte + 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 := &g.Level.Rooms[rm] + + // Set up the movement variables, in two cases: first drawing one down. + var rpt *Room + var del, turnDelta, spos, epos Coord + var distance, turnDistance int + if direc == 'd' { + rmt := rm + 3 // room # of dest + rpt = &g.Level.Rooms[rmt] // room pointer of dest + 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(IsGone) { // if not gone pick door pos + for { + spos.X = rpf.Pos.X + g.rnd(rpf.Max.X-2) + 1 + spos.Y = rpf.Pos.Y + rpf.Max.Y - 1 + if !(rpf.Flags.Has(IsMaze) && !g.Level.FlagsAt(spos.Y, spos.X).Has(FPass)) { + break + } + } + } + if !rpt.Flags.Has(IsGone) { + for { + epos.X = rpt.Pos.X + g.rnd(rpt.Max.X-2) + 1 + if !(rpt.Flags.Has(IsMaze) && !g.Level.FlagsAt(epos.Y, epos.X).Has(FPass)) { + break + } + } + } + distance = abs(spos.Y-epos.Y) - 1 // distance to move + turnDelta.Y = 0 // direction to turn + if spos.X < epos.X { + turnDelta.X = 1 + } else { + turnDelta.X = -1 + } + turnDistance = abs(spos.X - epos.X) // how far to turn + } else { // setup for moving right + rmt := rm + 1 + rpt = &g.Level.Rooms[rmt] + del = Coord{X: 1, Y: 0} + spos = rpf.Pos + epos = rpt.Pos + if !rpf.Flags.Has(IsGone) { + for { + spos.X = rpf.Pos.X + rpf.Max.X - 1 + spos.Y = rpf.Pos.Y + g.rnd(rpf.Max.Y-2) + 1 + if !(rpf.Flags.Has(IsMaze) && !g.Level.FlagsAt(spos.Y, spos.X).Has(FPass)) { + break + } + } + } + if !rpt.Flags.Has(IsGone) { + for { + epos.Y = rpt.Pos.Y + g.rnd(rpt.Max.Y-2) + 1 + if !(rpt.Flags.Has(IsMaze) && !g.Level.FlagsAt(epos.Y, epos.X).Has(FPass)) { + break + } + } + } + distance = abs(spos.X-epos.X) - 1 + if spos.Y < epos.Y { + turnDelta.Y = 1 + } else { + turnDelta.Y = -1 + } + turnDelta.X = 0 + turnDistance = abs(spos.Y - epos.Y) + } + + turnSpot := g.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.Flags.Has(IsGone) { + g.door(rpf, spos) + } else { + g.putpass(spos) + } + if !rpt.Flags.Has(IsGone) { + g.door(rpt, epos) + } else { + g.putpass(epos) + } + // Get ready to move... + curr := spos + for 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 == turnSpot { + for ; turnDistance > 0; turnDistance-- { + g.putpass(curr) + curr.X += turnDelta.X + curr.Y += turnDelta.Y + } + } + // Continue digging along + g.putpass(curr) + distance-- + } + curr.X += del.X + curr.Y += del.Y + if curr != epos { + g.msg("warning, connectivity problem on this level") + } +} + +// putpass adds a passage character or secret passage here (passages.c +// putpass). +func (g *RogueGame) putpass(cp Coord) { + pp := g.Level.At(cp.Y, cp.X) + pp.Flags.Set(FPass) + 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(IsMaze) { + 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 + } +} + +// passnum assigns a number to each passageway (passages.c passnum). +func (g *RogueGame) passnum() { + 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.numpass(rp.Exits[j].Y, rp.Exits[j].X) + } + } +} + +// numpass numbers a passageway square and its brethren (passages.c +// numpass). +func (g *RogueGame) numpass(y, x int) { + if x >= NumCols || x < 0 || y >= NumLines || y <= 0 { + return + } + fp := g.Level.FlagsAt(y, x) + if fp.Has(FPNum) { + 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 ch := g.Level.Char(y, x); ch == Door || + (!fp.Has(FReal) && (ch == '|' || ch == '-')) { + rp := &g.Level.Passages[g.pnum] + rp.Exits = append(rp.Exits, Coord{Y: y, X: x}) + } else if !fp.Has(FPass) { + return + } + *fp |= PlaceFlags(g.pnum) + // recurse on the surrounding places + g.numpass(y+1, x) + g.numpass(y-1, x) + g.numpass(y, x+1) + g.numpass(y, x-1) +} + +// abs is C abs() for ints. +func abs(n int) int { + if n < 0 { + return -n + } + return n +} diff --git a/game/potions.go b/game/potions.go new file mode 100644 index 0000000..be22871 --- /dev/null +++ b/game/potions.go @@ -0,0 +1,85 @@ +package game + +// potions.c — the visibility-related helpers arrive first; quaff and the +// potion effect dispatch come with the effects phase. + +// isMagic reports whether an object radiates magic (potions.c is_magic). +func (o *Object) isMagic() bool { + switch o.Type { + case Armor: + return o.Flags.Has(IsProt) || o.Arm != aClass[o.Which] + case Weapon: + return o.HPlus != 0 || o.DPlus != 0 + case Potion, Scroll, Stick, Ring, Amulet: + 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(CanSee) + for _, mp := range g.Level.Monsters { + if mp.On(IsInvis) && g.seeMonst(mp) && !g.Player.On(IsHalu) { + 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 !canSee { + g.standout() + } + if !g.Player.On(IsHalu) { + g.addch(mp.Type) + } else { + g.addch(byte(g.rnd(26) + 'A')) + } + if !canSee { + g.standend() + addNew = true + } + } + } + if turnOff { + g.Player.Flags.Clear(SeeMonst) + } else { + g.Player.Flags.Set(SeeMonst) + } + return addNew +} + +// 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(IsRun) { // visible and awake: + return true // it must have moved there + } + if g.Player.On(SeeMonst) && tp.OldCh == Stairs { + return true + } + } + return false +} diff --git a/game/rings.go b/game/rings.go new file mode 100644 index 0000000..80796a9 --- /dev/null +++ b/game/rings.go @@ -0,0 +1,174 @@ +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 := g.getItem("put on", int(Ring)) + // Make certain that it is something that we want to wear + if obj == nil { + return + } + if obj.Type != Ring { + if !g.Options.Terse { + g.msg("it would be difficult to wrap that around a finger") + } else { + g.msg("not a ring") + } + return + } + + // find out which hand to put it on + if g.isCurrent(obj) { + return + } + + var ring int + switch { + case p.CurRing[Left] == nil && p.CurRing[Right] == nil: + if ring = g.gethand(); ring < 0 { + return + } + case p.CurRing[Left] == nil: + ring = Left + case p.CurRing[Right] == nil: + ring = Right + default: + if !g.Options.Terse { + g.msg("you already have a ring on each hand") + } else { + g.msg("wearing two") + } + return + } + p.CurRing[ring] = obj + + // Calculate the effect it has on the poor guy. + switch obj.Which { + case RAddStr: + g.chgStr(obj.Arm) + case RSeeInvis: + g.invisOn() + case RAggr: + g.aggravate() + } + + if !g.Options.Terse { + g.addmsg("you are now wearing ") + } + g.msg("%s (%c)", g.invName(obj, true), obj.PackCh) +} + +// 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.invName(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") + } + } +} + +// 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). +var ringUses = [MaxRings]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 +} + +// 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 := ringUses[ring.Which] + if eat < 0 { + if g.rnd(-eat) == 0 { + eat = 1 + } else { + eat = 0 + } + } + if ring.Which == RDigest { + eat = -eat + } + return eat +} + +// ringNum prints ring bonuses (rings.c ring_num). +func ringNum(g *RogueGame, obj *Object) string { + if !obj.Flags.Has(IsKnow) { + return "" + } + switch obj.Which { + case RProtect, RAddStr, RAddDam, RAddHit: + return fmt.Sprintf(" [%s]", num(obj.Arm, 0, Ring)) + } + return "" +} diff --git a/game/rooms.go b/game/rooms.go new file mode 100644 index 0000000..76ade24 --- /dev/null +++ b/game/rooms.go @@ -0,0 +1,377 @@ +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 + +// doRooms creates rooms and corridors with a connectivity graph (rooms.c +// do_rooms). +func (g *RogueGame) doRooms() { + 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 i := 0; i < leftOut; i++ { + g.Level.Rooms[g.rndRoom()].Flags.Set(IsGone) + } + // dig and populate all the rooms on the level + for i := range g.Level.Rooms { + 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(IsGone) { + // Place a gone room. Make certain that there is a blank line + // for passage drawing. + 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 { + break + } + } + continue + } + // set room type + if g.rnd(10) < g.Depth-1 { + rp.Flags.Set(IsDark) // dark room + if g.rnd(15) == 0 { + rp.Flags = IsMaze // maze room + } + } + // Find a place and size for a random room + if rp.Flags.Has(IsMaze) { + 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-- + } + } else { + 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 { + break + } + } + } + g.drawRoom(rp) + // Put the gold in + if g.rnd(2) == 0 && (!g.HasAmulet || g.Depth >= g.MaxDepth) { + gold := newObject() + rp.GoldVal = g.goldCalc() + gold.SetGoldVal(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 = IsMany + gold.Group = goldGrp + gold.Type = Gold + attachObj(&g.Level.Objects, gold) + } + // Put the monster in + 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(IsMaze) { + g.doMaze(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, '-') + } +} + +// doMaze digs a maze (rooms.c do_maze). +func (g *RogueGame) doMaze(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.putpass(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 + del := [4]Coord{{X: 2, Y: 0}, {X: -2, Y: 0}, {X: 0, Y: 2}, {X: 0, Y: -2}} + + for { + 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(FPass) { + continue + } + if cnt++; g.rnd(cnt) == 0 { + nexty = newy + nextx = newx + } + } + if cnt == 0 { + return + } + g.accntMaze(y, x, nexty, nextx) + g.accntMaze(nexty, nextx, y, x) + var pos Coord + if nexty == y { + pos.Y = y + m.starty + if nextx-x < 0 { + pos.X = nextx + m.startx + 1 + } else { + pos.X = nextx + m.startx - 1 + } + } else { + pos.X = x + m.startx + if nexty-y < 0 { + pos.Y = nexty + m.starty + 1 + } else { + pos.Y = nexty + m.starty - 1 + } + } + g.putpass(pos) + pos.Y = nexty + m.starty + pos.X = nextx + m.startx + g.putpass(pos) + g.dig(nexty, nextx) + } +} + +// accntMaze accounts for maze exits (rooms.c accnt_maze). +func (g *RogueGame) accntMaze(y, x, ny, nx int) { + sp := &g.maze.maze[y][x] + for i := 0; i < sp.nexits; i++ { + 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} + } +} + +// rndPos picks a random spot in a room (rooms.c rnd_pos). +func (g *RogueGame) rndPos(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 (rooms.c find_floor with rp == NULL). +func (g *RogueGame) findFloor(rp *Room, limit int, monst bool) (Coord, bool) { + return g.findFloorImpl(rp, limit, monst, rp == nil) +} + +// 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) +} + +func (g *RogueGame) findFloorImpl(rp *Room, limit int, monst, pickroom bool) (Coord, bool) { + var compchar byte + if !pickroom { + compchar = Floor + if rp.Flags.Has(IsMaze) { + compchar = Passage + } + } + cnt := limit + for { + if limit != 0 { + if cnt--; cnt == -1 { + return Coord{}, false + } + } + if pickroom { + rp = &g.Level.Rooms[g.rndRoom()] + compchar = Floor + if rp.Flags.Has(IsMaze) { + compchar = Passage + } + } + cp := g.rndPos(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(IsDark) && !p.On(IsBlind) { + 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++ { + 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) + } + } else { + tp.OldCh = ch + if !g.seeMonst(tp) { + if p.On(SeeMonst) { + g.standout() + g.addch(tp.Disguise) + g.standend() + } else { + g.addch(ch) + } + } else { + g.addch(tp.Disguise) + } + } + } + } + } +} + +// 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(IsMaze) { + return + } + + var floor byte + switch { + case rp.Flags.Has(IsGone): + floor = Passage + case !rp.Flags.Has(IsDark) || p.On(IsBlind): + floor = Floor + default: + floor = ' ' + } + + p.Room = &g.Level.Passages[*g.Level.FlagsAt(cp.Y, cp.X)&FPNum] + 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.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) { + if p.On(SeeMonst) { + g.standout() + g.addch(ch) + g.standend() + break + } + pp := g.Level.At(y, x) + if pp.Ch == Door { + g.addch(Door) + } else { + g.addch(floor) + } + } + } + } + } + g.doorOpen(rp) +} diff --git a/game/screen.go b/game/screen.go new file mode 100644 index 0000000..a9446c2 --- /dev/null +++ b/game/screen.go @@ -0,0 +1,198 @@ +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). + ReadChar() byte +} + +// 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 +} + +func (w *Window) at(y, x int) *cell { return &w.cells[y*w.cols+x] } + +// 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() (y, x 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 := 0; i < len(s); i++ { + 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) +} + +// Printw writes formatted text at the cursor (curses printw). +func (w *Window) Printw(format string, a ...any) { + w.AddStr(fmt.Sprintf(format, a...)) +} + +// MvPrintw moves then writes formatted text (curses mvprintw). +func (w *Window) MvPrintw(y, x int, format string, a ...any) { + w.Move(y, x) + w.Printw(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) +} + +// 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 := 0; x < w.cols; x++ { + buf[x] = w.at(y, x).ch + } + return string(buf) +} + +// 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) + } +} + +// 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.Printw(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() } diff --git a/game/sticks.go b/game/sticks.go new file mode 100644 index 0000000..1c5de92 --- /dev/null +++ b/game/sticks.go @@ -0,0 +1,35 @@ +package game + +import "fmt" + +// sticks.c — wand and staff setup and naming. Zapping (do_zap, drain, +// fire_bolt) arrives with the combat phase. + +// fixStick sets up a new wand or staff (sticks.c fix_stick). +func (g *RogueGame) fixStick(cur *Object) { + if g.Items.WandType[cur.Which] == "staff" { + cur.Damage = "2x3" + } else { + cur.Damage = "1x1" + } + cur.HurlDmg = "1x1" + + switch cur.Which { + case WsLight: + cur.SetCharges(g.rnd(10) + 10) + default: + cur.SetCharges(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(IsKnow) { + return "" + } + if g.Options.Terse { + return fmt.Sprintf(" [%d]", obj.Charges()) + } + return fmt.Sprintf(" [%d charges]", obj.Charges()) +} diff --git a/game/things.go b/game/things.go new file mode 100644 index 0000000..25264b6 --- /dev/null +++ b/game/things.go @@ -0,0 +1,502 @@ +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) invName(obj *Object, drop bool) string { + var pb strings.Builder + which := obj.Which + it := &g.Items + switch obj.Type { + case Potion: + g.nameit(&pb, obj, "potion", it.PotColors[which], &it.Potions[which], nullstr) + case Ring: + g.nameit(&pb, obj, "ring", it.RingStones[which], &it.Rings[which], ringNum) + case Stick: + g.nameit(&pb, obj, it.WandType[which], it.WandMade[which], &it.Sticks[which], chargeStr) + case Scroll: + if obj.Count == 1 { + pb.WriteString("A scroll ") + } else { + fmt.Fprintf(&pb, "%d scrolls ", obj.Count) + } + op := &it.Scrolls[which] + if op.Know { + fmt.Fprintf(&pb, "of %s", op.Name) + } else if op.Guess != "" { + fmt.Fprintf(&pb, "called %s", op.Guess) + } else { + fmt.Fprintf(&pb, "titled '%s'", it.ScrNames[which]) + } + case Food: + if 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) + } + } else { + if obj.Count == 1 { + pb.WriteString("Some food") + } else { + fmt.Fprintf(&pb, "%d rations of food", obj.Count) + } + } + case Weapon: + sp := it.Weapons[which].Name + if obj.Count > 1 { + fmt.Fprintf(&pb, "%d ", obj.Count) + } else { + fmt.Fprintf(&pb, "A%s ", vowelstr(sp)) + } + if obj.Flags.Has(IsKnow) { + 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) + } + case Armor: + sp := it.Armors[which].Name + if obj.Flags.Has(IsKnow) { + fmt.Fprintf(&pb, "%s %s [", num(aClass[which]-obj.Arm, 0, Armor), sp) + if !g.Options.Terse { + pb.WriteString("protection ") + } + fmt.Fprintf(&pb, "%d]", 10-obj.Arm) + } else { + pb.WriteString(sp) + } + if obj.Label != "" { + fmt.Fprintf(&pb, " called %s", obj.Label) + } + case Amulet: + pb.WriteString("The Amulet of Yendor") + case Gold: + fmt.Fprintf(&pb, "%d Gold pieces", obj.GoldVal()) + } + + out := pb.String() + if g.InvDescribe { + p := &g.Player + if obj == p.CurArmor { + out += " (being worn)" + } + if obj == p.CurWeapon { + out += " (weapon in hand)" + } + if obj == p.CurRing[Left] { + out += " (on left hand)" + } else if obj == p.CurRing[Right] { + out += " (on right hand)" + } + } + if out != "" { + if drop && isUpper(out[0]) { + out = string(toLower(out[0])) + out[1:] + } else if !drop && isLower(out[0]) { + out = 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 := g.getItem("drop", 0) + if obj == nil { + return + } + if !g.dropCheck(obj) { + return + } + obj = g.leavePack(obj, true, !IsMult(obj.Type)) + // Link it into the level object list + attachObj(&g.Level.Objects, obj) + g.Level.SetChar(p.Pos.Y, p.Pos.X, obj.Type) + g.Level.FlagsAt(p.Pos.Y, p.Pos.X).Set(FDropped) + obj.Pos = p.Pos + if obj.Type == Amulet { + g.HasAmulet = false + } + g.msg("dropped %s", g.invName(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(IsCursed) { + g.msg("you can't. It appears to be cursed") + return false + } + if obj == p.CurWeapon { + p.CurWeapon = nil + } else if obj == p.CurArmor { + g.wasteTime() + p.CurArmor = nil + } else { + hand := Right + if obj == p.CurRing[Left] { + hand = Left + } + p.CurRing[hand] = nil + switch obj.Which { + case RAddStr: + g.chgStr(-obj.Arm) + case RSeeInvis: + g.unsee(0) + g.Extinguish(DUnsee) + } + } + return true +} + +// newThing returns a new random thing for the dungeon (things.c new_thing). +func (g *RogueGame) newThing() *Object { + cur := newObject() + cur.Damage = "0x0" + cur.HurlDmg = "0x0" + cur.Arm = 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.Type = Potion + cur.Which = pickOne(g, g.Items.Potions[:]) + case 1: + cur.Type = Scroll + cur.Which = pickOne(g, g.Items.Scrolls[:]) + case 2: + cur.Type = Food + g.Player.NoFood = 0 + if g.rnd(10) != 0 { + cur.Which = 0 + } else { + cur.Which = 1 + } + case 3: + g.initWeapon(cur, pickOne(g, g.Items.Weapons[:MaxWeapons])) + if r := g.rnd(100); r < 10 { + cur.Flags.Set(IsCursed) + cur.HPlus -= g.rnd(3) + 1 + } else if r < 15 { + cur.HPlus += g.rnd(3) + 1 + } + case 4: + cur.Type = Armor + cur.Which = pickOne(g, g.Items.Armors[:]) + cur.Arm = aClass[cur.Which] + if r := g.rnd(100); r < 20 { + cur.Flags.Set(IsCursed) + cur.Arm += g.rnd(3) + 1 + } else if r < 28 { + cur.Arm -= g.rnd(3) + 1 + } + case 5: + cur.Type = Ring + cur.Which = pickOne(g, g.Items.Rings[:]) + switch cur.Which { + case RAddStr, RProtect, RAddHit, RAddDam: + if cur.Arm = g.rnd(3); cur.Arm == 0 { + cur.Arm = -1 + cur.Flags.Set(IsCursed) + } + case RAggr, RTeleport: + cur.Flags.Set(IsCursed) + } + case 6: + cur.Type = Stick + cur.Which = pickOne(g, g.Items.Sticks[:]) + g.fixStick(cur) + } + return cur +} + +// 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.addmsg("for ") + } + g.addmsg("what type") + if !g.Options.Terse { + g.addmsg(" 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.Type = typ + obj.Which = order[i] + g.addLine("%s", g.invName(&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 + prompt := "--Press space to continue--" + 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 { + if !isFlush && line != "" { + if g.msg("%s", line) == Escape { + return Escape + } + } + pg.lineCnt++ + } else { + if !pg.init { + pg.maxlen = len(prompt) + pg.init = true + } + if pg.lineCnt >= NumLines-1 || isFlush { + if g.Options.InvType == InvOver && isFlush && !pg.newpage { + // Overlay the accumulated list in a box at the top right + // of the screen, prompt, and restore what was beneath. + 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() + } 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) + } + 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 + } + } + return ^Escape +} + +// 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 = "potion" + case Scroll: + tystr = "scroll" + case Ring: + tystr = "ring" + 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) { + if 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) + } + } else if obj.Count == 1 { + fmt.Fprintf(pb, "A%s %s %s", vowelstr(which), which, typ) + } else { + fmt.Fprintf(pb, "%d %s %ss", obj.Count, which, typ) + } +} + +// nullstr returns an empty string (things.c nullstr). +func nullstr(*RogueGame, *Object) string { return "" } diff --git a/game/weapons.go b/game/weapons.go new file mode 100644 index 0000000..723d58c --- /dev/null +++ b/game/weapons.go @@ -0,0 +1,86 @@ +package game + +import "fmt" + +// weapons.c — weapon initialization and naming. The throwing half +// (missile, do_motion, hit_monster) arrives with the combat phase. + +const noWeapon = -1 + +// initWeaps is the weapons.c init_dam[] table. +var initWeaps = [MaxWeapons]struct { + dam string // damage when wielded + hrl string // damage when thrown + launch int // launching weapon + flags ObjFlags +}{ + {"2x4", "1x3", noWeapon, 0}, // Mace + {"3x4", "1x2", noWeapon, 0}, // Long sword + {"1x1", "1x1", noWeapon, 0}, // Bow + {"1x1", "2x3", Bow, IsMany | IsMissl}, // Arrow + {"1x6", "1x4", noWeapon, IsMissl}, // Dagger + {"4x4", "1x2", noWeapon, 0}, // 2h sword + {"1x1", "1x3", noWeapon, IsMany | IsMissl}, // Dart + {"1x2", "2x4", noWeapon, IsMany | IsMissl}, // Shuriken + {"2x3", "1x6", noWeapon, IsMissl}, // Spear +} + +// initWeapon sets up a new weapon (weapons.c init_weapon). +func (g *RogueGame) initWeapon(weap *Object, which int) { + iwp := &initWeaps[which] + weap.Type = Weapon + weap.Which = which + weap.Damage = iwp.dam + weap.HurlDmg = iwp.hrl + weap.Launch = iwp.launch + weap.Flags = iwp.flags + weap.HPlus = 0 + weap.DPlus = 0 + if which == Dagger { + weap.Count = g.rnd(4) + 2 + weap.Group = g.Items.Group + g.Items.Group++ + } else if weap.Flags.Has(IsMany) { + weap.Count = g.rnd(8) + 8 + weap.Group = g.Items.Group + g.Items.Group++ + } else { + 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 +}