This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
oubliette/internal/shell/adventure/adventure.go
Torjus Håkestad aa569aac16 feat: add text adventure shell (PLAN.md 3.4)
Zork-style dungeon crawler set in an abandoned data center / medieval dungeon.
11 rooms, 6 items, 3 puzzles (dark room, locked door, maintenance panel),
standard text adventure parser with verb aliases and direction shortcuts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 05:13:03 +01:00

115 lines
2.8 KiB
Go

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 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
}