Zork – Items & Inventory

It’s great to be able to explore our game world, but personally I don’t want to feel like a window shopper. There is “stuff” in this world and I want to interact with it. By the the end of this lesson we will be able to take things or put them back, drop them where we stand, or keep them around and brag about our growing inventory.

Let’s Take Stuff

Sure I can open the mailbox, but what good is it if I cant take out the leaflet? When you find out how worthless it is, can you put it back? Can you drop it? All of these kinds of interactions are an important set of interactions which are based on the entity containing a “Takeable” component.

Takeable

Add this structure to wrap the Takeable table in our databse:

import Foundation
import SQLite

struct Takeable {
    
    // MARK: - Fields
    static let component_id: Int64 = 8
    static let table = Table("Takeables")
    static let takeable_id_column = Expression<Int64>("id")
    static let can_take_column = Expression<Bool>("can_take")
    static let message_column = Expression<String?>("message")
    let row: Row
    
    // MARK: - Properties
    var id: Int64 {
        get {
            return row[Takeable.takeable_id_column]
        }
    }
    
    var canTake: Bool {
        get {
            return row[Takeable.can_take_column]
        }
    }
    
    var message: String? {
        get {
            return row[Takeable.message_column]
        }
    }
    
    var entity: Entity? {
        get {
            return Entity.fetchByComponentID(Takeable.component_id, dataID: id)
        }
    }
}

extension Entity {
    func getTakeable() -> Takeable? {
        guard let component = getComponent(Takeable.component_id) else { return .None }
        let table = Takeable.table.filter(Takeable.takeable_id_column == component.dataID)
        guard let result = DataManager.instance.prepare(table).first else { return .None }
        return Takeable(row: result)
    }
}

Takeable System

In practice, taking an object is nothing more than modifying its container entity id. For the player to pick up an item, you use the player’s id. To put an item in or on another entity, you use the container’s id. To simply drop an entity, you use the room’s id. Of course we can add some additional methods to wrap it all up and provide some better messaging.

import Foundation
import SQLite

class TakeableSystem {
    class func setup() {
        InterpreterSystem.instance.register(TakeAction())
        InterpreterSystem.instance.register(PutAction())
        InterpreterSystem.instance.register(DropAction())
    }
    
    class func take(takeable: Takeable) -> String {
        guard let containable = takeable.entity?.getContainable(), inventory = PlayerSystem.player.getContainer() where takeable.canTake else {
            return takeable.message ?? "You can't take that."
        }
        
        guard ContainmentSystem.canContain(containable, container: inventory) else {
            return "You can't hold any more."
        }
        
        ContainmentSystem.move(containable, containingEntityID: PlayerSystem.player.id)
        return takeable.message ?? "Taken."
    }
    
    class func put(takeable: Takeable, container: Container) -> String {
        guard let containable = takeable.entity?.getContainable(), containerID = container.entity?.id else {
            return "You can't do that."
        }
        
        guard ContainmentSystem.canContain(containable, container: container) else {
            return "It wont fit."
        }
        
        ContainmentSystem.move(containable, containingEntityID: containerID)
        return "Done."
    }
    
    class func drop(takeable: Takeable) -> String {
        guard let containable = takeable.entity?.getContainable(), roomID = RoomSystem.room.entity?.id else { return "You can't do that." }
        ContainmentSystem.move(containable, containingEntityID: roomID)
        return "Done."
    }
}

Containment System

We can’t just go around taking anything and everything in our world – eventually there is too much for you to hold. Likewise, you can’t stuff an elephant in a mailbox. Not that Zork or my project has an elephant, but you get the idea. Takeable items have a size, and whatever it is that needs to hold a thing should have a concept of its own capacity. Let’s go ahead and open up the Containment system so we can account for these new aspects.

class func canContain(containable: Containable, container: Container) -> Bool {
    let occupiedSpace = ContainmentSystem.occupiedSpace(container)
    let hasRoom = container.capacity - occupiedSpace >= containable.size
    return hasRoom
}

Before the player can take an item and put it in his inventory, and before a user could put an item in another container, we must check whether or not the new container has enough room. If your inventory is already full, you can’t grab that shiny set of heavy armor, sorry. As long as the size of the containable plus the currently occupied space within the container is less than the containers capacity, it will be able to hold the specified entity.

class func occupiedSpace(container: Container) -> Double {
    var usedSpace: Double = 0
    guard let containerEntity = container.entity else { return usedSpace }
    let contents = ContainmentSystem.fetchContainedEntities(containerEntity)
    for entity in contents {
        guard let containable = entity.getContainable() else { continue }
        usedSpace += containable.size
    }
    return usedSpace
}

The way that we determine how much space is currently occupied in a container, we fetch all of its contained entities and sum up the size of each.

class func move(containable: Containable, containingEntityID: Int64) {
    let update = Containable.table.filter(Containable.containable_id_column == containable.id).update(Containable.containing_entity_id_column <- containingEntityID)
    DataManager.instance.run(update)
}

The “move” method allows you to move an entity from one container entity to another by specifying an entity ID for which to move it to.

class func checkContainment(entity: Entity, containingEntity: Entity?) -> Bool {
    guard let containable = entity.getContainable() else { return false }
    guard let containingEntity = containingEntity else { return false }
    guard containable.containingEntityID == containingEntity.id else { return false }
    return true
}

This convenience method allows the game to determine whether or not a container already holds a specified entity. This way you can print a snarky message when you do something as dumb as trying to take an object you are already holding.

Take Action

This action is actually a bit complex because there are several methods by which I want to support the taking of an item. I will support:

  • Take – no target (show the no target error)
  • Take – one or more primary targets and no secondary targets
  • Take From – one or more primary targets and a single secondary target

We will begin with a base action that actually allows us to take an entity from a container:

private class BaseTakeAction: BaseAction {
    static let takeErrors = ["What a concept!", "An interesting idea...", "You can't be serious.", "A valiant attempt."]
    
    func randomTakeError() -> String {
        let randomIndex = Int(arc4random_uniform(UInt32(BaseTakeAction.takeErrors.count)))
        return BaseTakeAction.takeErrors[randomIndex]
    }
    
    func customTakeFiltering(target: Target) {
        target.candidates = target.candidates.filter({ $0.getTakeable() != nil })
        if target.candidates.count == 0 {
            target.error = randomTakeError()
            return
        }
    }
    
    func takeTarget(target: Target, containerTarget: Target?) -> String {
        TargetingSystem.filter(target, options: [TargetingFilter.CurrentRoom, TargetingFilter.ContainerIsOpen, TargetingFilter.NotHeldByPlayer])
        customTakeFiltering(target)
        TargetingSystem.validate(target)
        
        guard let match = target.match, takeable = match.getTakeable() where target.error == nil else {
            return target.error ?? "\(containerTarget): You can't do that."
        }
        
        if let containerTarget = containerTarget {
            guard ContainmentSystem.checkContainment(match, containingEntity: containerTarget.match) else {
                return "The \(target.userInput) isn't in the \(containerTarget.userInput)"
            }
        }
        
        let message = TakeableSystem.take(takeable)
        return "\(target.userInput): \(message)"
    }
}

The “takeTarget” method does the heavy lifting. It makes sure to filter an interpretations candidates to the current room, and to being located within an open container where applicable. It also verifies that you aren’t already holding whatever it is you want to take. Next it performs a custom filter to verify that the entity actually has a “Takeable” component attached. Otherwise we have an opportunity to tease the player with one of my random “takeErrors”. Finally we verify that if a container was specified by the player, that it does actually hold the entity we wish to take. When everything checks out we let the TakeableSystem attempt to perform its magic.

private class TakeTargetAction: BaseTakeAction {
    override func handle(interpretation: Interpretation) {
        var message = ""
        for (index, target) in interpretation.primary.enumerate() {
            if index > 0 {
                message += "\n"
            }
            message += takeTarget(target, containerTarget: .None)
        }
        
        LoggingSystem.instance.addLog(message)
    }
}

The “TakeTargetAction” is a simple action that loops through all of the primary targets and uses the base class method to try and take the entity. We dont have to worry about the container, because the filter system will at least have validated that we can see what it is that we wish to take.

private class TakeTargetFromAction: BaseTakeAction {
    override func handle(interpretation: Interpretation) {
        guard let containerTarget = interpretation.secondary.first else { return }
        TargetingSystem.filter(containerTarget, options: [TargetingFilter.CurrentRoom, TargetingFilter.ContainerIsOpen])
        TargetingSystem.validate(containerTarget)
        
        guard let _ = containerTarget.match where containerTarget.error == nil else {
            guard let error = containerTarget.error else { return }
            LoggingSystem.instance.addLog(error)
            return
        }
        
        var message = ""
        for (index, target) in interpretation.primary.enumerate() {
            if index > 0 {
                message += "\n"
            }
            message += takeTarget(target, containerTarget: containerTarget)
        }
        
        LoggingSystem.instance.addLog(message)
    }
}

The “TakeTargetFromAction” is similar, but comes from an interpretation where the player also specified a container. In this case we also will have to run some validation on the container itself to make sure that it is also located within the current room, and is open (if applicable).

class TakeAction: CompoundAction {
    init() {
        let noTarget = NoTargetErrorAction(commands: ["TAKE"])
        let target = TakeTargetAction(commands: ["TAKE"], specifiers: [], primaryTargetMode: .OneOrMore, secondaryTargetMode: .Zero)
        let targetFrom = TakeTargetFromAction(commands: ["TAKE"], specifiers: ["FROM"], primaryTargetMode: .OneOrMore, secondaryTargetMode: .Single)
        super.init(actions: [noTarget, target, targetFrom])
    }
}

Finally we get to the compound version which sticks everything together. This is the action which will be registered to our interpreter.

Put Action

The put command is similar to the take command. We will support the command without a target (and show the no target error), but if we provide a primary target and no secondary target we still need to show an error. We have to know where to put an entity in order to complete the action.

private class PutTargetAction: BaseAction {
    override func handle(interpretation: Interpretation) {
        LoggingSystem.instance.addLog("Where do you want to put it?")
    }
}

The “PutTargetAction” handles the scenario where the player requested to put an entity without clarifying where to put it.

private class PutTargetInAction: BaseAction {
    func customTakeFiltering(target: Target) {
        target.candidates = target.candidates.filter({ $0.getTakeable() != nil })
        if target.candidates.count == 0 {
            target.error = "\(target.userInput): You can't do that."
            return
        }
    }
    
    private func customPutFiltering(target: Target) {
        target.candidates = target.candidates.filter({ $0.getContainer() != nil })
        if target.candidates.count == 0 {
            target.error = "You can't put anything in the \(target.userInput)."
            return
        }
    }
    
    override func handle(interpretation: Interpretation) {
        guard let containerTarget = interpretation.secondary.first else { return }
        
        TargetingSystem.filter(containerTarget, options: [TargetingFilter.CurrentRoom, TargetingFilter.ContainerIsOpen])
        customPutFiltering(containerTarget)
        TargetingSystem.validate(containerTarget)
        
        guard let containerEntity = containerTarget.match, container = containerEntity.getContainer() where containerTarget.error == nil else {
            guard let error = containerTarget.error else { return }
            LoggingSystem.instance.addLog(error)
            return
        }
        
        var message: String = ""
        
        for (index, target) in interpretation.primary.enumerate() {
            if index > 0 {
                message += "\n"
            }
            
            TargetingSystem.filter(target, options: TargetingFilter.HeldByPlayer)
            customTakeFiltering(target)
            TargetingSystem.validate(target)
            
            guard let match = target.match, takeable = match.getTakeable() where target.error == nil else {
                if let error = target.error { message += error }
                continue
            }
            
            let result = TakeableSystem.put(takeable, container: container)
            message += "\(target.userInput): \(result)"
        }
        
        LoggingSystem.instance.addLog(message)
    }
}

This action is able to take over when we have both a primary and secondary target. Like with the “Take” action, we need to verify both sets of targets are located within the room in open containers and when everything is valid, we can let the TakeableSystem handle the actual “put” of the entity.

class PutAction: CompoundAction {
    init() {
        let noTarget = NoTargetErrorAction(commands: ["PUT"])
        let noContainer = PutTargetAction(commands: ["PUT"], specifiers: [], primaryTargetMode: .OneOrMore, secondaryTargetMode: .Zero)
        let targetIn = PutTargetInAction(commands: ["PUT"], specifiers: ["IN", "ON"], primaryTargetMode: .OneOrMore, secondaryTargetMode: .Single)
        super.init(actions: [noTarget, noContainer, targetIn])
    }
}

The “PutAction” forms the compound version for this command that we will actually register with the interpreter system.

Drop Action

Dropping an item is much simpler than taking or putting an entity. We already know the current container (the player) and the destination container (the room) – or at least that is what they should be.

This action’s filter process is able to be as simple as merely verifying that the entity you wish to drop is currently held by the player.

import Foundation

class DropAction: CompoundAction {
    init() {
        let noTarget = NoTargetErrorAction(commands: ["DROP"])
        let target = DropTargetAction(commands: ["DROP"], specifiers: [], primaryTargetMode: .OneOrMore, secondaryTargetMode: .Zero)
        super.init(actions: [noTarget, target])
    }
}

private class DropTargetAction: BaseAction {
    override func handle(interpretation: Interpretation) {
        var message: String = ""
        
        for (index, target) in interpretation.primary.enumerate() {
            if index > 0 {
                message += "\n"
            }
            
            TargetingSystem.filter(target, options: TargetingFilter.HeldByPlayer)
            TargetingSystem.validate(target)
            
            guard let match = target.match, takeable = match.getTakeable() where target.error == nil else {
                if let error = target.error { message += error }
                continue
            }
            
            let result = TakeableSystem.drop(takeable)
            message += "\(target.userInput): \(result)"
        }
        
        LoggingSystem.instance.addLog(message)
    }
}

Inventory

We can take stuff! Yay! But in your mad rush to run around and grab everything you find you might forget what all you have taken. Now what? Ah, let’s provide a way to show our inventory.

Inventory Action

As you may have guessed, we will use the “inventory” command to display the list of items the player is holding. Here is what that looks like:

import Foundation

class InventoryAction: CompoundAction {
    init() {
        let action = ShowInventoryAction(commands: ["INVENTORY"], specifiers: [], primaryTargetMode: .Zero, secondaryTargetMode: .Zero)
        super.init(actions: [action])
    }
}

private class ShowInventoryAction: BaseAction {
    override func handle(interpretation: Interpretation) {
        let items = SightSystem.listContainerContents(PlayerSystem.player)
        guard items.count > 0 else {
            LoggingSystem.instance.addLog("You don't have anything.")
            return
        }
        let message = "You're holding:\n  \(items.joinWithSeparator("\n  "))"
        LoggingSystem.instance.addLog(message)
    }
}

Player System

Since the inventory is technically related to the player, let’s add a setup method to this system which will handle registering our new command:

class func setup() {
    InterpreterSystem.instance.register(InventoryAction())
}

Master System

Dont forget that when another system requires setup that we must add it to our Master system. Our master system’s setup now looks like this:

class func setup() {
    SightSystem.setup()
    PortalSystem.setup()
    OpenableSystem.setup()
    PlayerSystem.setup()
    TakeableSystem.setup()
}

Demo

Build and run your project. The game will be more fun now – I promise. After opening the mailbox you can actually take the leaflet! You can put it back too. Or drop it. Oh, and you can see your inventory too. Woot!

 photo ECS_Zork_08_01_Demo_zpsozx8lcsh.png

Summary

In this lesson we added the models, systems and actions necessary to handle the ability to take entities in the game world. A taken item appears in your inventory, and will be removed from your inventory if you put the item back or drop it.

Don’t forget that this project is accompanied by a repository HERE. There will be one commit per lesson so you can see exactly how the project would have been cofigured and how the code would look at each step.

Advertisements

3 thoughts on “Zork – Items & Inventory

  1. Awesome series. Can’t wait for the next one.

    Question:
    How would we handle Containers that can hold items on top of them?

    For the moment the system would incorrectly describe an object thats supposed to be ON a table as an object IN it.

    Will there be WEARABLES in a future post?
    Could these be stored on a player in a different kind of inventory? I’m thinking about expanding this idea with more RPG elements. Dice rolls, armour with defence stats, weapons with damage, etc.

    An extra challenge I’ve set myself is to try and procedurally generate a world to navigate through.

    Thanks for the time you’ve put into these. Very helpful.

    Liked by 1 person

    1. Thanks, I’m glad you’ve enjoyed it!

      Regarding the ‘On’ vs ‘In’ – there are lots of options how you could polish this. Just like I had a variety of fields for each component – such as a capacity for a container, you could add another field indicating ‘how’ the items were held. I might go this route if the only difference for implementation was truly the use of that word ‘on’.

      On the other hand, you could expand it a little further and differentiate a container vs some other kind of component, perhaps we could call it a ‘surface’ or some other name that might be more appropriate. This would have its own system and it would just know to use the word ‘on’. It could have different fields like an amount of weight it could hold instead of capacity in the sense the area it can hold.

      There aren’t wearables in this 9-part initial series, but hopefully the building blocks it does have can give you a good foundation so you could expand it in any direction you like. A wearable wouldn’t be that much different than the concept of the inventory system. It might just use its own component and system, but would probably need a few more systems to really shine like a combat system.

      I’m always available to bounce ideas off of, especially on the forum since it helps keep threads together better, but good luck on your challenges – they sound fun!

      Like

      1. Thanks for the detailed reply. Once I’ve finished the series I’ll start experimenting.
        The best thing about this system seems to be that its very easy to scale.

        Thanks again.

        Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s