From 43b4fbe7462761774daca2b54c87e97573fae92b Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 22 Jul 2026 21:53:02 +0700 Subject: [PATCH] Decompose save.go (refactor step 7) saveGame splits into askDefaultSave/saveFileName/saveCheckOverwrite/ askOverwrite around a saveAnswer tri-state; snapshot gains destRefFor; applySnapshot gains applyMonsters/applyDests. save.go is complexity-clean. Behavior unchanged. --- game/save.go | 266 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 163 insertions(+), 103 deletions(-) diff --git a/game/save.go b/game/save.go index 3a648ce..e69b065 100644 --- a/game/save.go +++ b/game/save.go @@ -275,43 +275,44 @@ func (g *RogueGame) snapshot() *SaveState { } st.Monsters = append(st.Monsters, sc) - - ref := destRef{} - - switch { - case m.Dest == nil: - case m.Dest == &p.Pos: - ref = destRef{Kind: 1} - default: - for mi, om := range g.Level.Monsters { - if m.Dest == &om.Pos { - ref = destRef{Kind: 2, Idx: mi} - } - } - - if ref.Kind == 0 { - for _, oo := range g.Level.Objects { - if m.Dest == &oo.Pos { - ref = destRef{Kind: 3, Idx: objAt[oo]} - } - } - } - - if ref.Kind == 0 { - for ri := range g.Level.Rooms { - if m.Dest == &g.Level.Rooms[ri].Gold { - ref = destRef{Kind: 4, Idx: ri} - } - } - } - } - - st.Dests = append(st.Dests, ref) + st.Dests = append(st.Dests, g.destRefFor(m, objAt)) } return st } +// destRefFor encodes a monster's chase target as a (kind, index) +// reference: the hero, another monster, a level object, or room gold +// (state.c rs_write_thing). +func (g *RogueGame) destRefFor(m *Monster, objAt map[*Object]int) destRef { + switch { + case m.Dest == nil: + return destRef{} + case m.Dest == &g.Player.Pos: + return destRef{Kind: 1} + } + + for mi, om := range g.Level.Monsters { + if m.Dest == &om.Pos { + return destRef{Kind: 2, Idx: mi} + } + } + + for _, oo := range g.Level.Objects { + if m.Dest == &oo.Pos { + return destRef{Kind: 3, Idx: objAt[oo]} + } + } + + for ri := range g.Level.Rooms { + if m.Dest == &g.Level.Rooms[ri].Gold { + return destRef{Kind: 4, Idx: ri} + } + } + + return destRef{} +} + // applySnapshot rebuilds live game state from a snapshot. func (g *RogueGame) applySnapshot(st *SaveState) { p := &g.Player @@ -412,7 +413,15 @@ func (g *RogueGame) applySnapshot(st *SaveState) { p.MaxStats = sp.MaxStats p.VfHit = sp.VfHit - // monsters, their map index, and their chase targets + g.applyMonsters(st) + g.applyDests(st) + + g.scr.Std.SetContents(st.Screen) +} + +// applyMonsters rebuilds the monster list and its map index from a +// snapshot (the monster half of applySnapshot). +func (g *RogueGame) applyMonsters(st *SaveState) { g.Level.Monsters = nil for i := range st.Monsters { sc := &st.Monsters[i] @@ -430,13 +439,17 @@ func (g *RogueGame) applySnapshot(st *SaveState) { g.Level.Monsters = append(g.Level.Monsters, m) g.Level.SetMonsterAt(m.Pos.Y, m.Pos.X, m) } +} +// applyDests re-aims the monsters' chase targets from their (kind, +// index) references (the fixup half of applySnapshot). +func (g *RogueGame) applyDests(st *SaveState) { for i, ref := range st.Dests { m := g.Level.Monsters[i] switch ref.Kind { case 1: - m.Dest = &p.Pos + m.Dest = &g.Player.Pos case 2: m.Dest = &g.Level.Monsters[ref.Idx].Pos case 3: @@ -445,13 +458,20 @@ func (g *RogueGame) applySnapshot(st *SaveState) { m.Dest = &g.Level.Rooms[ref.Idx].Gold } } - - g.scr.Std.SetContents(st.Screen) } +// saveAnswer is a yes/no/escape prompt result in the save-game flow. +type saveAnswer int + +// The saveGame prompt outcomes. +const ( + saveYes saveAnswer = iota + saveNo + saveAbort +) + // saveGame implements the "save game" command (save.c save_game). The -// labeled prompt loop and the useDefault flag stand in for the C -// goto over/gotfile flow. +// labeled prompt loop stands in for the C goto over/gotfile flow. func (g *RogueGame) saveGame() { g.Msgs.Mpos = 0 @@ -460,84 +480,34 @@ prompt: useDefault := false if g.FileName != "" { - var c byte - - for { - g.msg("save file (%s)? ", g.FileName) - c = g.readchar() - - g.Msgs.Mpos = 0 - if c == Escape { - g.msg("") - - return - } - - if c == 'n' || c == 'N' || c == 'y' || c == 'Y' { - break - } - - g.msg("please answer Y or N") + a := g.askDefaultSave() + if a == saveAbort { + return } - if c == 'y' || c == 'Y' { - g.addstr("Yes\n") - g.refresh() - - useDefault = true - } + useDefault = a == saveYes } for { - var buf string - if useDefault { - buf = g.FileName - useDefault = false - } else { - g.Msgs.Mpos = 0 - g.msg("file name: ") - - if g.getStr(&buf, g.scr.Std) == Quit { - g.msg("") - - return - } - - g.Msgs.Mpos = 0 + buf, ok := g.saveFileName(useDefault) + if !ok { + return } - // test to see if the file exists - _, statErr := os.Stat(buf) - if statErr == nil { - for { - g.msg("File exists. Do you wish to overwrite it?") - g.Msgs.Mpos = 0 - c := g.readchar() - if c == Escape { - g.msg("") + useDefault = false - return - } + a := g.saveCheckOverwrite(buf) + if a == saveAbort { + return + } - if c == 'y' || c == 'Y' { - break - } - - if c == 'n' || c == 'N' { - continue prompt // the C goto over: start again - } - - g.msg("Please answer Y or N") - } - - g.msg("file name: %s", buf) - _ = os.Remove(g.FileName) // best effort, as in C (md_unlink) + if a == saveNo { + continue prompt // the C goto over: start again } g.FileName = buf - err := g.saveFile(g.FileName) - if err != nil { + if err := g.saveFile(g.FileName); err != nil { g.msg("%s", err.Error()) continue @@ -550,6 +520,96 @@ prompt: g.myExit() } +// askDefaultSave asks whether to save to the current file name (save.c +// save_game). +func (g *RogueGame) askDefaultSave() saveAnswer { + for { + g.msg("save file (%s)? ", g.FileName) + c := g.readchar() + + g.Msgs.Mpos = 0 + + switch c { + case Escape: + g.msg("") + + return saveAbort + case 'y', 'Y': + g.addstr("Yes\n") + g.refresh() + + return saveYes + case 'n', 'N': + return saveNo + } + + g.msg("please answer Y or N") + } +} + +// saveFileName picks the save path: the default, or a prompted one; ok +// is false when the player quit the prompt (save.c save_game). +func (g *RogueGame) saveFileName(useDefault bool) (string, bool) { + if useDefault { + return g.FileName, true + } + + g.Msgs.Mpos = 0 + g.msg("file name: ") + + buf := "" + if g.getStr(&buf, g.scr.Std) == Quit { + g.msg("") + + return "", false + } + + g.Msgs.Mpos = 0 + + return buf, true +} + +// saveCheckOverwrite guards an existing file: saveNo restarts the whole +// prompt, saveAbort quits (save.c save_game). +func (g *RogueGame) saveCheckOverwrite(buf string) saveAnswer { + // test to see if the file exists + if _, statErr := os.Stat(buf); statErr != nil { + return saveYes + } + + answer := g.askOverwrite() + if answer != saveYes { + return answer + } + + g.msg("file name: %s", buf) + _ = os.Remove(g.FileName) // best effort, as in C (md_unlink) + + return saveYes +} + +// askOverwrite asks whether to overwrite the existing file (save.c +// save_game). +func (g *RogueGame) askOverwrite() saveAnswer { + for { + g.msg("File exists. Do you wish to overwrite it?") + g.Msgs.Mpos = 0 + + switch g.readchar() { + case Escape: + g.msg("") + + return saveAbort + case 'y', 'Y': + return saveYes + case 'n', 'N': + return saveNo + } + + g.msg("Please answer Y or N") + } +} + // saveFile writes the saved game (save.c save_file). A failed write means // a corrupt save, so the file is removed before reporting the error. func (g *RogueGame) saveFile(path string) error {