package adventure import ( "context" "errors" "fmt" "io" "strings" "time" "git.t-juice.club/torjus/oubliette/internal/shell" ) const sessionTimeout = 10 * time.Minute // AdventureShell implements a Zork-style text adventure set in a dungeon/data center. type AdventureShell struct{} // NewAdventureShell returns a new AdventureShell instance. func NewAdventureShell() *AdventureShell { return &AdventureShell{} } func (a *AdventureShell) Name() string { return "adventure" } func (a *AdventureShell) Description() string { return "Zork-style text adventure dungeon crawler" } func (a *AdventureShell) Handle(ctx context.Context, sess *shell.SessionContext, rw io.ReadWriteCloser) error { ctx, cancel := context.WithTimeout(ctx, sessionTimeout) defer cancel() dungeonName := configString(sess.ShellConfig, "dungeon_name", "THE OUBLIETTE") game := newGame() // Print banner and initial room. banner := strings.ReplaceAll(adventureBanner(dungeonName), "\n", "\r\n") fmt.Fprint(rw, banner) // Show starting room. startDesc := game.describeRoom(game.rooms[game.currentRoom]) startDesc = strings.ReplaceAll(startDesc, "\n", "\r\n") fmt.Fprintf(rw, "%s\r\n\r\n", startDesc) for { if _, err := fmt.Fprint(rw, "> "); err != nil { return nil } line, err := shell.ReadLine(ctx, rw) if errors.Is(err, io.EOF) { fmt.Fprint(rw, "\r\n") return nil } if err != nil { return nil } trimmed := strings.TrimSpace(line) if trimmed == "" { continue } result := game.dispatch(trimmed) var output string if result.output != "" { output = result.output output = strings.ReplaceAll(output, "\r\n", "\n") output = strings.ReplaceAll(output, "\n", "\r\n") fmt.Fprintf(rw, "%s\r\n", output) } // Log command and output to store. if sess.Store != nil { if err := sess.Store.AppendSessionLog(ctx, sess.SessionID, trimmed, output); err != nil { return fmt.Errorf("append session log: %w", err) } } if sess.OnCommand != nil { sess.OnCommand("adventure") } if result.exit { return nil } } } func adventureBanner(dungeonName string) string { return fmt.Sprintf(` ___ _ _ ____ _ ___ _____ _____ _____ / _ \| | | | __ )| | |_ _| ____|_ _|_ _| ___ | | | | | | | _ \| | | || _| | | | | / _ \ | |_| | |_| | |_) | |___ | || |___ | | | | | __/ \___/ \___/|____/|_____|___|_____| |_| |_| \___| Welcome to %s. You wake up in the dark. The air is cold and hums with electricity. This is a place where things are put to be forgotten. Type 'help' for commands. Type 'look' to examine your surroundings. `, dungeonName) } // configString reads a string from the shell config map with a default. func configString(cfg map[string]any, key, defaultVal string) string { if cfg == nil { return defaultVal } if v, ok := cfg[key]; ok { if s, ok := v.(string); ok && s != "" { return s } } return defaultVal }