// Copyright 2018 Andrew 'Diddymus' Rolfe. All rights reserved. // // Released under: Free Public License 1.0.0 // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION // OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // Package main implements a small, simple, single player, plain text, 'toy' // adventure game. It is intended to be used for learning, experimenting, // general hacking and having fun while doing so. // // Commands available to the player are: // // N or NORTH, S or SOUTH, E or EAST, W or WEST - for movement // L or LOOK - describe the current location again // SNEEZE - makes the player sneeze // QUIT - exit the game // // To run the game simply open a terminal or command window, change to the // directory/folder where this file is saved, then type following: // // go run cottage.go // // Have fun, and hack the code :) package main // Import some packages from the standard library we want to use. import ( "bufio" "fmt" "os" "strings" ) // Constants for direction indexes. These are used to define the directions and // directionNames. They also let us use North, East, South and West instead of // the cryptic numbers 0, 1, 2 and 3 in the code. const ( North = iota East South West directionCount // Always last item, will equal the number of directions ) // directions for mapping short or long direction names to direction indexes. // For example directions["E"] will be equal to the constant East (which is 1). var directions = map[string]int{ "N": North, "NORTH": North, "E": East, "EAST": East, "S": South, "SOUTH": South, "W": West, "WEST": West, } // directionNames for mapping direction indexes to descriptive direction names. // For example directionNames[East] would be equal to the string "east". var directionNames = map[int]string{ North: "north", East: "east", South: "south", West: "west", } // location is used to define places in the world. The exits are a map keyed by // direction and pointing to a location reference. For example: // // map[int]string{South: "sidepath"} // // Indicates an exit South, leading to the location with reference "sidepath". // By using string references instead of *location we avoid having to have a // two pass setup: One to define the locations and the second to link up the // exits. It also means we can quickly and easily find a location in the world // map for any exit. See the loadWorld method for some more details. type location struct { name string // Short location name description string // Longer location description exits map[int]string // Exits from this location to other locations } // world contains all of the locations in the game world. Each location is // keyed by a reference. This means each location reference must be unique. // This is the same reference that is used to map exits to other locations. var world map[string]location // player represents the user in the game. type player struct { where string // Where we are, a reference to a location input string // Current input from the user quit bool // Are we asking to quit? *bufio.Reader // Where we read the user's input from *bufio.Writer // Where we write the user's output to } // main performs initialisation and then runs the main game loop until the // player quits. The calls to Flush are needed to make sure all output is // written out to the user before asking for input. func main() { world = loadWorld() p := newPlayer("path") p.greeting() p.processCommand("LOOK") for !p.quit { p.msg("?") p.Flush() p.input, _ = p.ReadString('\n') p.processInput() } p.Flush() } // newPlayer initialises a new player and places them at the given starting // location. func newPlayer(start string) *player { return &player{ start, "", false, bufio.NewReader(os.Stdin), bufio.NewWriter(os.Stdout), } } // msg is used to write a message to be displayed to the player. func (p *player) msg(format string, a ...interface{}) { fmt.Fprintf(p, format, a...) } // processInput takes a player's input and passes it to process for // handling. func (p *player) processInput() { cmd := strings.ToUpper(strings.TrimSpace(p.input)) p.processCommand(cmd) } // process handles the given command. This is separate from processInput so // that it can be called directly. See main, which calls processCommand("LOOK") // to display the initial location without any input from the user. Also see // move which uses processCommand("LOOK") to display the location moved to. func (p *player) processCommand(cmd string) { switch cmd { case "": // no command, maybe player just pressed enter? case "L", "LOOK": p.look() case "N", "E", "S", "W", "NORTH", "EAST", "SOUTH", "WEST": p.move(cmd) case "SNEEZE": p.sneeze() case "QUIT": p.quit = true p.msg("\nBye bye!\n\n") default: p.msg("Eh?\n") } } // Greeting displays a simple welcome message to the player. func (p *player) greeting() { p.msg("\nWelcome to the cottage!\n\n") } // sneeze makes the player sneeze! An example of a very simple command. func (p *player) sneeze() { p.msg("Achoo! You sneeze.\n") } // look describes the current location. As exits are stored in a map, ranging // over them will result in the exits having a random order. We therefore loop // over the direction indexes for consistent ordering and test for each exit. func (p *player) look() { where := world[p.where] p.msg("[%s]\n%s\n\n", where.name, where.description) p.msg("You can see exits:") // Examine the exits for the location for dir := 0; dir <= directionCount; dir++ { if _, ok := where.exits[dir]; ok { p.msg(" %s", directionNames[dir]) } } p.msg("\n") } // move attempts to follow an exit to a new location. func (p *player) move(direction string) { // Find direction index from its name dir := directions[direction] // Find the specific exit where the player is exit := world[p.where].exits[dir] // Check for an exit in the desired direction if exit == "" { p.msg("You can't go %s!\n", directionNames[dir]) return } // Check new location actually exists if _, ok := world[exit]; !ok { p.msg("Oops, you can't actually go %s.\n", directionNames[dir]) return } // Move player and describe new location p.where = exit p.processCommand("LOOK") } // loadWorld creates all of the locations in the world. Here we are populating // the world directly, the format matches the definition of location: // // "reference": { // "short name", // "longer description", // map[int]string{ // direction: "to reference", // direction: "to reference", // Repeat for each exit as needed // }, // }, // // For example: // // "shed": { // "Shed", // "You are standing in a small, dark, dingy garden shed.", // map[int]string{ // South: "sidepath", // }, // }, // // This defines a location with a reference of "shed". The short description is // "Shed" and the long description is "You are standing in a small, dark, dingy // garden shed.", there is a single exit South which leads to the location with // a reference of "sidepath". // // NOTE: If you use the same reference more than once, only the last one will // be useable and the others will be overwritten! func loadWorld() map[string]location { return map[string]location{ "path": { "Path outside a cottage", "You are on a path leading north to a small, white cottage.", map[int]string{ North: "frontdoor", South: "bogusExit", // South leads to a non-existent location here West: "frontlawn", }, }, "frontlawn": { "Font lawn", "You are standing on a small, neat lawn outside the cottage.", map[int]string{ North: "sidepath", East: "path", }, }, "frontdoor": { "Front door", "You at standing at the open front door of a small, white cottage.", map[int]string{ North: "southhall", South: "path", }, }, "southhall": { "Hallway", "You are in a hallway by the front door.", map[int]string{ North: "northhall", East: "kitchen", South: "frontdoor", West: "study", }, }, "northhall": { "Hallway", "You are in a hallway. Some pictures hang on the wall.", map[int]string{ North: "bathroom", South: "southhall", West: "bedroom", }, }, "kitchen": { "Kitchen", "You are standing in a small, very tidy kitchen.", map[int]string{ North: "backlawn", West: "southhall", }, }, "study": { "Study", "This is a small, cosy study with a desk and many bookcases.", map[int]string{ East: "southhall", }, }, "bedroom": { "Bedroom", "You are in a small bedroom with a bed and a large wardrobe.", map[int]string{ East: "northhall", }, }, "bathroom": { "Bathroom", "You are in a small, very clean and bright bathroom.", map[int]string{ South: "northhall", }, }, "backlawn": { "Back lawn", "You are standing on a small, neat lawn at the back of the cottage.", map[int]string{ South: "kitchen", West: "sidepath", }, }, "sidepath": { "Side path", "You are on a narrow path running along the side of the cottage.", map[int]string{ North: "shed", East: "backlawn", South: "frontlawn", }, }, "shed": { "Shed", "You are standing in a small, dark, dingy garden shed.", map[int]string{ South: "sidepath", }, }, } }