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>
359 lines
9.4 KiB
Go
359 lines
9.4 KiB
Go
package adventure
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
type gameState struct {
|
|
currentRoom string
|
|
inventory []string
|
|
rooms map[string]*room
|
|
items map[string]*item
|
|
flags map[string]bool
|
|
turns int
|
|
}
|
|
|
|
type commandResult struct {
|
|
output string
|
|
exit bool
|
|
}
|
|
|
|
func newGame() *gameState {
|
|
rooms, items := newWorld()
|
|
return &gameState{
|
|
currentRoom: "oubliette",
|
|
rooms: rooms,
|
|
items: items,
|
|
flags: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
func (g *gameState) dispatch(input string) commandResult {
|
|
cmd := parseCommand(input)
|
|
if cmd.verb == "" {
|
|
return commandResult{}
|
|
}
|
|
|
|
g.turns++
|
|
|
|
switch cmd.verb {
|
|
case "look":
|
|
return g.cmdLook(cmd.object)
|
|
case "go":
|
|
return g.cmdGo(cmd.object)
|
|
case "take":
|
|
return g.cmdTake(cmd.object)
|
|
case "drop":
|
|
return g.cmdDrop(cmd.object)
|
|
case "use":
|
|
return g.cmdUse(cmd.object)
|
|
case "inventory":
|
|
return g.cmdInventory()
|
|
case "help":
|
|
return g.cmdHelp()
|
|
case "quit":
|
|
return commandResult{output: "The darkness closes in. Session terminated.", exit: true}
|
|
default:
|
|
return commandResult{output: fmt.Sprintf("I don't understand '%s'. Type 'help' for available commands.", input)}
|
|
}
|
|
}
|
|
|
|
func (g *gameState) cmdHelp() commandResult {
|
|
help := `Available commands:
|
|
look / examine [item] - Look around or examine something
|
|
go <direction> - Move (north, south, east, west, up, down)
|
|
take <item> - Pick up an item
|
|
drop <item> - Drop an item
|
|
use <item> - Use an item
|
|
inventory - Check what you're carrying
|
|
help - Show this help
|
|
quit / exit - End session
|
|
|
|
You can also just type a direction (n, s, e, w, u, d) to move.`
|
|
return commandResult{output: help}
|
|
}
|
|
|
|
func (g *gameState) cmdLook(object string) commandResult {
|
|
r := g.rooms[g.currentRoom]
|
|
|
|
// Look at a specific item.
|
|
if object != "" {
|
|
return g.examineItem(object)
|
|
}
|
|
|
|
// Look at the room.
|
|
return commandResult{output: g.describeRoom(r)}
|
|
}
|
|
|
|
func (g *gameState) describeRoom(r *room) string {
|
|
var b strings.Builder
|
|
|
|
fmt.Fprintf(&b, "== %s ==\n", r.name)
|
|
|
|
// Dark room check.
|
|
if r.darkDesc != "" && !g.hasItem("flashlight") {
|
|
b.WriteString(r.darkDesc)
|
|
return b.String()
|
|
}
|
|
|
|
b.WriteString(r.description)
|
|
|
|
// List visible items.
|
|
visibleItems := g.roomItems(r)
|
|
if len(visibleItems) > 0 {
|
|
b.WriteString("\n\nYou can see: ")
|
|
names := make([]string, len(visibleItems))
|
|
for i, id := range visibleItems {
|
|
names[i] = g.items[id].name
|
|
}
|
|
b.WriteString(strings.Join(names, ", "))
|
|
}
|
|
|
|
// List exits.
|
|
if len(r.exits) > 0 {
|
|
b.WriteString("\n\nExits: ")
|
|
dirs := make([]string, 0, len(r.exits))
|
|
for dir := range r.exits {
|
|
dirs = append(dirs, dir)
|
|
}
|
|
b.WriteString(strings.Join(dirs, ", "))
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (g *gameState) roomItems(r *room) []string {
|
|
// In dark rooms without flashlight, can't see items.
|
|
if r.darkDesc != "" && !g.hasItem("flashlight") {
|
|
return nil
|
|
}
|
|
return r.items
|
|
}
|
|
|
|
func (g *gameState) examineItem(name string) commandResult {
|
|
id := g.resolveItem(name)
|
|
if id == "" {
|
|
return commandResult{output: fmt.Sprintf("You don't see '%s' here.", name)}
|
|
}
|
|
return commandResult{output: g.items[id].description}
|
|
}
|
|
|
|
func (g *gameState) cmdGo(direction string) commandResult {
|
|
if direction == "" {
|
|
return commandResult{output: "Go where? Try a direction: north, south, east, west, up, down."}
|
|
}
|
|
|
|
r := g.rooms[g.currentRoom]
|
|
destID, ok := r.exits[direction]
|
|
if !ok {
|
|
return commandResult{output: fmt.Sprintf("You can't go %s from here.", direction)}
|
|
}
|
|
|
|
// Check locked doors.
|
|
if r.locked != nil {
|
|
if flag, locked := r.locked[direction]; locked && !g.flags[flag] {
|
|
return g.lockedMessage(direction)
|
|
}
|
|
}
|
|
|
|
g.currentRoom = destID
|
|
dest := g.rooms[destID]
|
|
|
|
// Entering the exit room ends the game.
|
|
if destID == "exit" {
|
|
return commandResult{output: g.describeRoom(dest), exit: true}
|
|
}
|
|
|
|
return commandResult{output: g.describeRoom(dest)}
|
|
}
|
|
|
|
func (g *gameState) lockedMessage(direction string) commandResult {
|
|
if direction == "east" && g.currentRoom == "archive" {
|
|
return commandResult{output: "The steel door won't budge. The keycard reader blinks red, waiting."}
|
|
}
|
|
return commandResult{output: "The way is locked."}
|
|
}
|
|
|
|
func (g *gameState) cmdTake(name string) commandResult {
|
|
if name == "" {
|
|
return commandResult{output: "Take what?"}
|
|
}
|
|
|
|
r := g.rooms[g.currentRoom]
|
|
|
|
// Can't take items in dark rooms.
|
|
if r.darkDesc != "" && !g.hasItem("flashlight") {
|
|
return commandResult{output: "It's too dark to find anything."}
|
|
}
|
|
|
|
id := g.resolveRoomItem(name)
|
|
if id == "" {
|
|
return commandResult{output: fmt.Sprintf("You don't see '%s' here.", name)}
|
|
}
|
|
|
|
it := g.items[id]
|
|
if !it.takeable {
|
|
return commandResult{output: fmt.Sprintf("You can't take the %s.", it.name)}
|
|
}
|
|
|
|
// Remove from room, add to inventory.
|
|
g.removeRoomItem(r, id)
|
|
g.inventory = append(g.inventory, id)
|
|
return commandResult{output: fmt.Sprintf("Taken: %s", it.name)}
|
|
}
|
|
|
|
func (g *gameState) cmdDrop(name string) commandResult {
|
|
if name == "" {
|
|
return commandResult{output: "Drop what?"}
|
|
}
|
|
|
|
id := g.resolveInventoryItem(name)
|
|
if id == "" {
|
|
return commandResult{output: fmt.Sprintf("You're not carrying '%s'.", name)}
|
|
}
|
|
|
|
// Remove from inventory, add to room.
|
|
g.removeInventoryItem(id)
|
|
r := g.rooms[g.currentRoom]
|
|
r.items = append(r.items, id)
|
|
return commandResult{output: fmt.Sprintf("Dropped: %s", g.items[id].name)}
|
|
}
|
|
|
|
func (g *gameState) cmdUse(name string) commandResult {
|
|
if name == "" {
|
|
return commandResult{output: "Use what?"}
|
|
}
|
|
|
|
id := g.resolveInventoryItem(name)
|
|
if id == "" {
|
|
return commandResult{output: fmt.Sprintf("You're not carrying '%s'.", name)}
|
|
}
|
|
|
|
switch id {
|
|
case "keycard":
|
|
return g.useKeycard()
|
|
case "rusty_key":
|
|
return g.useRustyKey()
|
|
case "flashlight":
|
|
return commandResult{output: "The flashlight is already on. Its beam cuts through the darkness."}
|
|
case "ethernet_cable":
|
|
return commandResult{output: "You wave the cable around hopefully. Nothing happens. Both ends are chewed through anyway."}
|
|
case "floppy_disk":
|
|
return commandResult{output: "You don't have anything to put it in. Floppy drives went extinct decades ago."}
|
|
case "note":
|
|
return commandResult{output: g.items[id].description}
|
|
default:
|
|
return commandResult{output: fmt.Sprintf("You can't figure out how to use the %s here.", g.items[id].name)}
|
|
}
|
|
}
|
|
|
|
func (g *gameState) useKeycard() commandResult {
|
|
if g.currentRoom != "archive" {
|
|
return commandResult{output: "There's nothing to use the keycard on here."}
|
|
}
|
|
if g.flags["archive_unlocked"] {
|
|
return commandResult{output: "You already unlocked that door."}
|
|
}
|
|
g.flags["archive_unlocked"] = true
|
|
return commandResult{output: "You swipe the keycard. The reader flashes green and the steel door clicks open with a heavy thunk."}
|
|
}
|
|
|
|
func (g *gameState) useRustyKey() commandResult {
|
|
if g.currentRoom != "generator" {
|
|
return commandResult{output: "There's nothing to use the rusty key on here."}
|
|
}
|
|
if g.flags["panel_opened"] {
|
|
return commandResult{output: "The maintenance panel is already open."}
|
|
}
|
|
g.flags["panel_opened"] = true
|
|
return commandResult{output: `The key fits. The maintenance panel swings open with a screech, revealing a logbook:
|
|
|
|
MAINTENANCE LOG - GENERATOR B
|
|
Last service: 2003-11-15
|
|
Technician: J. Dunwich
|
|
|
|
Notes: "Generator A offline permanently. B running on fumes.
|
|
Fuel delivery canceled - 'facility decommissioned' per management.
|
|
But the servers are still running. WHO IS USING THEM?
|
|
Filed ticket #4,271. No response. As usual."
|
|
|
|
The entries stop after that date.`}
|
|
}
|
|
|
|
func (g *gameState) cmdInventory() commandResult {
|
|
if len(g.inventory) == 0 {
|
|
return commandResult{output: "You're not carrying anything."}
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString("You are carrying:\n")
|
|
for _, id := range g.inventory {
|
|
fmt.Fprintf(&b, " - %s\n", g.items[id].name)
|
|
}
|
|
return commandResult{output: strings.TrimRight(b.String(), "\n")}
|
|
}
|
|
|
|
// hasItem checks if the player has an item in inventory.
|
|
func (g *gameState) hasItem(id string) bool {
|
|
return slices.Contains(g.inventory, id)
|
|
}
|
|
|
|
// resolveItem finds an item by name in both room and inventory.
|
|
func (g *gameState) resolveItem(name string) string {
|
|
if id := g.resolveInventoryItem(name); id != "" {
|
|
return id
|
|
}
|
|
return g.resolveRoomItem(name)
|
|
}
|
|
|
|
// resolveRoomItem finds an item in the current room by partial name match.
|
|
func (g *gameState) resolveRoomItem(name string) string {
|
|
r := g.rooms[g.currentRoom]
|
|
name = strings.ToLower(name)
|
|
for _, id := range r.items {
|
|
if matchesItem(id, g.items[id].name, name) {
|
|
return id
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// resolveInventoryItem finds an item in inventory by partial name match.
|
|
func (g *gameState) resolveInventoryItem(name string) string {
|
|
name = strings.ToLower(name)
|
|
for _, id := range g.inventory {
|
|
if matchesItem(id, g.items[id].name, name) {
|
|
return id
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// matchesItem checks if a search term matches an item's ID or display name.
|
|
func matchesItem(id, displayName, search string) bool {
|
|
return id == search ||
|
|
strings.ToLower(displayName) == search ||
|
|
strings.Contains(id, search) ||
|
|
strings.Contains(strings.ToLower(displayName), search)
|
|
}
|
|
|
|
func (g *gameState) removeRoomItem(r *room, id string) {
|
|
for i, itemID := range r.items {
|
|
if itemID == id {
|
|
r.items = append(r.items[:i], r.items[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *gameState) removeInventoryItem(id string) {
|
|
for i, invID := range g.inventory {
|
|
if invID == id {
|
|
g.inventory = append(g.inventory[:i], g.inventory[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|