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 - Move (north, south, east, west, up, down) take - Pick up an item drop - Drop an item use - 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 } } }