Writing a Text Adventure Game in Go - Part 3

<<< Part 2: Combat Part 4: Putting it all together >>>

Intro (Items and Treasure)

The last time I promised to merge all the code and create items that could be used in the text adventure, unfortunately I have to apologize because of my other current commitments I could only get some of that done. So in this installment I will show you how to create items that can be in a specific location and make some items dependant on the adventurer having other items on him/her to use them. So Let’s get started.

Part 3 - Items and Treasure

Declare structs and variables for Items

Ok so the first thing we need to do is create an Item structure and then a list of items, just like we did for Weapons, We will use an integer to denote the which items exist in our text adventure world. Here is the code:

type Item struct {
	Name       string
	Action     string
	ItemForUse int
	Contains   []int
}

var Items = map[int]*Item{
	1: {Name: "Key"},
	2: {Name: "Chest", ItemForUse: 1, Contains: []int{3}},
	3: {Name: "Medal"},
}

We also need to add items to players and locations so I simply added:
Items []int
to the Player and Location structures.

Right after I started to figure how I could get the users to pick up items and open them it became apparent that I was going to have to parse user input instead of just getting numbers input like in the first two tutorials. Given this I need a func to get a full line of user input….

func GetUserStrInput() string {
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("\n >>> ")
	text, _ := reader.ReadString('\n')
	fmt.Println(text)
	return text
}

Loop and Parse

Now that we have a full line of user input we need to parse what the user wants to do, the following is a very crude parser and it does not have much in the way of error checking. In the following code we are looping until the user is tired of playing (which won’t take long), while printing the objects in this single room adventure, we then get input from the user and parse the command and parameters.

//To be refactored on a location struct
func describeItems(player Character) {
	l := locMap[player.CurrentLocation]

	DisplayInfo("You see:")
	for _, itm := range l.Items {
		DisplayInfof("\t%s\n", Items[itm].Name)
	}
}


func ProcessCommands(player Character, input string) string {
	tokens := strings.Fields(input)
	command := tokens[0]
	itemName := ""
	if len(tokens) > 1 {
		itemName = tokens[1]
	}
	DisplayInfo(tokens)
	loc := locMap[player.CurrentLocation]
	switch command {
	case "get":
		//Make sure we do not pick it up twice
		if ItemInRoom(loc, itemName) && !ItemOnPlayer(player, itemName) {
			PutItemInPlayerBag(player, itemName)
			ItemRemoveFromRoom(loc, itemName)
		} else {
			DisplayInfo("Could not get " + itemName)
		}
	case "open":
		OpenItem(player, itemName)
	case "inv":
		DisplayInfo("Your Inventory: ")
		for _, itm := range player.Items {
			DisplayInfo("\t" + Items[itm].Name)
		}
	default:
	}
	return command
}

func main() {
	player = *new(Character)
	player.CurrentLocation = "main"
	input := ""
	for input != "quit" {
		describeItems(player)
		input = GetUserStrInput()
		input = ProcessCommands(player, input)
	}
}

Support Code

I am sure after looking into that code you realized that we need a few supporting funcs to perform or determine things like:

  1. Does the user have a required item?
  2. Is the Item in this location?
  3. Remove item from location
  4. Remove item from another item
  5. Add item to user’s inventory
  6. Open something and reveal what is inside.

Here are these funcs:

func OpenItem(pla Character, itemName string) {
	DisplayInfo("Opening " + itemName)
	loc := locMap[player.CurrentLocation]
	for _, itm := range loc.Items {
		if Items[itm].Name == itemName {
			DisplayInfo("Loop >> " + Items[itm].Name)
			if Items[itm].ItemForUse != 0 && PlayerHasItem(pla, Items[itm].ItemForUse) {
				loc.Items = append(loc.Items, Items[itm].Contains...)
				Items[itm].Contains = *new([]int)
			}
		} else {
			DisplayInfo("Could not open the " + itemName)
		}
	}
}

func PlayerHasItem(pla Character, itm int) bool {
	for _, pitm := range pla.Items {
		if pitm == itm {
			return true
		}
	}
	return false
}

//To be refactored on a character struct
func PutItemInPlayerBag(pla Character, itemName string) {
	for index, itm := range Items {
		if itm.Name == itemName {
			player.Items = append(player.Items, index)
			return
		}
	}
}

//To be refactored on a location struct
func ItemRemoveFromRoom(loc *Location, itemName string) {
	for index, itm := range loc.Items {
		if Items[itm].Name == itemName {
			loc.Items = append(loc.Items[:index], loc.Items[index+1:]...)
		}
	}
}

//To be refactored on a location struct
func ItemInRoom(loc *Location, itemName string) bool {
	for _, itm := range loc.Items {
		if Items[itm].Name == itemName {
			return true
		}
	}
	return false
}

//To be refactored on a character struct
func ItemOnPlayer(pla Character, itemName string) bool {
	for _, itm := range pla.Items {
		if Items[itm].Name == itemName {
			return true
		}
	}
	return false
}

Final words

You can see how the code is layed out for this week’s installment at this Gist. So far in this series we have created a world where the player can walk about, then we create a combat system and now we have an Item system. Putting all these together will give us a full fledged functioning text adventure system.

So as I promised before in the next installment we will put all this together do a lot of refactoring to make the code cleaner and more modular.

See you then and in the meantime Have a great week!!!

GO! GO! GO!

<<< Part 2: Combat Part 4: Putting it all together >>>

*** Sign up for my email list to keep in touch with all the interesting new happenings in the go community with the GolangNewsFeed

comments powered by Disqus