Previously, we created a “User Action” which demonstrated how our interpreter could map out a sentence. In this lesson we will expand the concept of an action with some reusable classes. Then we will implement a few actions which are actually intended to be used by the game. By the end of this lesson you will be able to interact with the mailbox in your starting room by opening or closing it. This also means we will actually be modifying some data in the database for the first time.
More Models
In order to complete this lesson we will only need to implement one more model. Like before, it is very similar to the models from previous lessons such as the “Entity” or “Noun” component which are there to wrap the setup of a table in the database.
Container
An entity which can conceptually hold another entity. For example, a mailbox holds a letter, or a plate of food is held by a table. Containers take into account their own capacity as well as the size of the items they contain.
import Foundation import SQLite struct Container { // MARK: - Fields static let component_id: Int64 = 3 static let table = Table("Containers") static let container_id_column = Expression<Int64>("id") static let capacity_column = Expression<Double>("capacity") let row: Row // MARK: - Properties var id: Int64 { get { return row[Container.container_id_column] } } var capacity: Double { get { return row[Container.capacity_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(Container.component_id, dataID: id) } } } extension Entity { func getContainer() -> Container? { guard let component = getComponent(Container.component_id) else { return .None } let table = Container.table.filter(Container.container_id_column == component.dataID) guard let result = DataManager.instance.prepare(table).first else { return .None } return Container(row: result) } }
Systems
In preparation of the ability to open and close an openable component, we will add and modify some more systems.
Containment System
Open the containment system and append the following snippet. This method allows me to find all of the containable entities that are contained by a specified entity. This is true regardless of if the containing entity has a container component.
class func fetchContainedEntities(entity: Entity) -> [Entity] { let table = Containable.table.filter(Containable.containing_entity_id_column == entity.id) let containables = DataManager.instance.prepare(table).map({ Containable(row: $0) }) var result: [Entity] = [] for item in containables { guard let entity = item.entity else { continue } result.append(entity) } return result }
Sight System
Let’s add a new system which will describe actions as they take place. For example, when you open a container, what do you see? Later this system will also describe a room as you enter it and will also support new actions like “look” and “examine”.
import Foundation import SQLite class SightSystem { class func describeOpening(openable: Openable) -> String? { guard let entity = openable.entity else { return .None } let containerName = LabelSystem.describe(entity) let items = SightSystem.listContainerContents(entity) guard items.count > 0 else { return .None } let message = items.count == 1 ? "Opening the \(containerName) reveals a \(items[0])." : "Opening the \(containerName) reveals:\n \(items.joinWithSeparator("\n "))" return message } class func listContainerContents(entity: Entity) -> [String] { guard let _ = entity.getContainer() else { return [] } if let openable = entity.getOpenable() { guard openable.isOpen else { return [] } } let contents = ContainmentSystem.fetchContainedEntities(entity) var items: [String] = [] for item in contents { let name = LabelSystem.describe(item) items.append(name) } return items } }
The “describeOpening” method formats a message for what the user sees after opening a given entity. If the entity doesnt contain anything then no message will be provided. Otherwise, there will be a message formatted based on whether there is only one, or if there are more than one entities contained. The “listContainerContents” method helps by fetching all of the contained entities and returning an array of the description of each. This method will also be reused at various other times such as when inspecting a player’s inventory.
Openable System
We will add another new system to handle the openable component. This means it will be able to both open and close the entities which have it, assuming it isn’t locked in place. The openable component has a trigger which we will implement in a future lesson.
import Foundation import SQLite class OpenableSystem { class func setup() { InterpreterSystem.instance.register(OpenAction()) InterpreterSystem.instance.register(CloseAction()) } // TODO: Add Code Here }
This method will be called as part of a setup process for the overall game. Many systems will have a setup method, and we will use this opportunity to register some user actions with the interpreter system. The “OpenAction” and “CloseAction” classes will be presented in a bit, but for now you can know that they are separate objects which implement the UserAction protocol, and have provided special “handle” methods to execute the opening and closing of entities with an Openable component.
class func open(item: Openable) -> String { guard item.isOpen == false else { return "It is already open." } guard item.isLocked == false else { return item.openMessage ?? "You can't open it." } setOpen(item, value: true) return item.openMessage ?? (SightSystem.describeOpening(item) ?? "Opened.") }
Once the “OpenAction” has determined that a user’s input is valid in the attempt to open something, the action will invoke this method to finish the job. The system knows the specifics about the component and can provide a message that indicates what has happened (or not happened) as a result of the command. For example, if you try to open something that is already open, you get a message that it was already open – no action was actually needed. If the target is locked, then you are presented either with the message attached to the component, or a default message otherwise. Finally, if you actually open the target, then you will either see a custom message from the component, a description from the sight system, or a default message – whichever is valid first.
class func close(item: Openable) -> String { guard item.isOpen == true else { return "It is already closed." } guard item.isLocked == false else { return item.closeMessage ?? "You can't close it." } setOpen(item, value: false) return item.closeMessage ?? "Closed." }
The close method is basically the inverse of the open method – it has the same kinds of checks for the entity already being closed, or locked etc.
private class func setOpen(item: Openable, value: Bool) { let update = Openable.table.filter(Openable.openable_id_column == item.id).update(Openable.is_open_column <- value) DataManager.instance.run(update) } private class func setLock(item: Openable, value: Bool) { let update = Openable.table.filter(Openable.openable_id_column == item.id).update(Openable.is_locked_column <- value) DataManager.instance.run(update) }
The last two methods provide us an opportunity to actually modify data in the database. You can toggle the “isOpen” or “isLocked” values for the row in the database using the appropriate update method. We first start with the Openable table, then filter it down to a single entity within that table (by an id) otherwise our change would apply to every row of the table. The change happens with the “update” using a special operator “<-" that allows the expression (named column on the left hand side) to be updated according to the value (on the right hand side). We use the data manager to actually run the udpate.
Master System
The “Openable System” wont be the only system supporting actions. Therefore I created this system to handle registering all of the systems. If any system had a dependency or required a special order for setup then this will be the location to handle it. For now, all we need to do is call setup:
import Foundation class MasterSystem { class func setup() { OpenableSystem.setup() } }
Actions
We have created a protocol named “UserAction” which declares a set of commands and specifiers which the action expects to work with. Its true purpose is to attempt to take an interpretation, validate it, and apply it to a game system. In a more modern game, I wouldn’t necessarily need this step, or at least not in the same way, because you could do something like click an item on the screen to attempt to take it. If you don’t see the item, you cant click on it, so it is in a way a self-validated process. In our case, you can type any word in any sentence, regardless of whether or not you should be able to take the actions you input, and regardless of whether or not the object targets are located within the same room, etc.
Base Action
Create a new script called BaseAction and copy the contents below:
import Foundation enum TargetMode { case Any // Matches any case case Zero // Expects exactly zero targets case UpToOne // Expects zero or one target case Single // Expects exactly one target case OneOrMore // Expects one or more targets case Multiple // Expects more than one target } class BaseAction: UserAction { // TODO: Add code here }
To help make the action a little more flexibile and reusable I would like to specify information on the quantity of targets that are also expected. For example, when we implement the “Open” action, we know that we will need “something” to try to open. If there is no “primary target” provided in the interpretation, then we can either abort early with a custom message or hand the interpretation off to something else.
There are a variety of expectations I may have on an action, or a specific implementation of an action, based on the specific number of targets, so I provided an enumeration called “TargetMode” to help make the expecation more readable.
static let invalidSentence = "That sentence isn't one I recognize." let commands: Set<String> let specifiers: Set<String> let primaryTargetMode: TargetMode let secondaryTargetMode: TargetMode
Add these fields inside of the BaseAction class body. I provided an “invalidSentence” which can be logged anytime the user attempts something you don’t specifically handle. Next I added fields for a set of commands and a set of specifiers, both of which are required by the “UserAction” protocol. Finally, I added a field that allows us to specify the “TargetMode” for both the primary target and secondary target of an interpretation.
init(commands: Set<String>, specifiers: Set<String>, primaryTargetMode: TargetMode, secondaryTargetMode: TargetMode) { self.commands = commands self.specifiers = specifiers self.primaryTargetMode = primaryTargetMode self.secondaryTargetMode = secondaryTargetMode }
I provided an initializer for our class that requires all commands, specifiers and target modes to be set. The intention here is that you have a custom command already configured for every scenario you wish to handle, rather than trying to adapt an action dynamically as you play.
func canHandle(interpretation: Interpretation) -> Bool { guard commands.contains(interpretation.command) else { return false } if specifiers.count > 0 && !specifiers.contains(interpretation.specifier) { return false } if interpretation.specifier.characters.count > 0 && !specifiers.contains(interpretation.specifier) { return false } guard checkMatch(interpretation.primary, mode: primaryTargetMode) else { return false } guard checkMatch(interpretation.secondary, mode: secondaryTargetMode) else { return false } return true } func checkMatch(targets: [Target], mode: TargetMode) -> Bool { switch mode { case .Any: return true case .Zero: return targets.count == 0 case .UpToOne: return targets.count <= 1 case .Single: return targets.count == 1 case .OneOrMore: return targets.count >= 1 case .Multiple: return targets.count > 1 } }
These methods compare the data in an interpretation with the values of the action to see if the expectations match. The “canHandle” method can be used before or during a “handle” method as a way to exit early if necessary. The “checkMatch” method examines the targets provided in an interpretation against a specified target mode. I pulled it into its own method so it could be reused for both primary and secondary target mode validation.
func handle(interpretation: Interpretation) { }
In order to complete the “UserAction” protocol we must provide an implementation for the “handle” method, even if the implementation is blank. Subclasses will provide better handler methods.
Compound Action
Sometimes you may wish to use the same command word with different scenarios. For example, if I type “look” then the command can infer to just look around in general or at the room itself, but if I type “look at {an object}” then it would look at the specified object instead. Furthermore, you may decide to support synonyms for your command which may require different specifiers for natural language. I might say “examine {an object}” which didn’t need the “at” preposition, but “look {an object}” just sounds funny. To support these we will create a subclass of our “BaseAction” that can provide handlers for any of these scenarios.
import Foundation class CompoundAction: BaseAction { // TODO: Add code here }
Add a new script named “CompoundAction” and make it a subclass of the “BaseAction” class from before.
let actions: [BaseAction] let fallback: ((Interpretation) -> ())?
The compound action is constructed as a collection of other actions. It attempts to handle an interpretation by letting each of the actions in its array handle it. In the event that none of the actions in the array can handle it, there is also a fallback block which provides one last opportunity to take some sort of action.
init (actions: [BaseAction], fallback:((Interpretation) -> ())? = .None) { self.actions = actions self.fallback = fallback var allCommands: Set<String> = [] var allSpecifiers: Set<String> = [] for action in actions { allCommands.unionInPlace(action.commands) allSpecifiers.unionInPlace(action.specifiers) } super.init(commands: allCommands, specifiers: allSpecifiers, primaryTargetMode: .Any, secondaryTargetMode: .Any) }
The initializer requires the array of actions with which to build the compound action, but the fallback block is optional and defaults to nothing. Since the compound action inherits from BaseAction, it will need to provide its own set of commands and specifiers. All of the commands and specifiers from all of the actions in the array are grouped into a single set and passed to the base class constructor. We also specify “.Any” for the targeting modes because we dont want to restrict the ways an action in the array could handle an interpretation. We will still have all of the more specific settings per action that we can check later.
override func canHandle(interpretation: Interpretation) -> Bool { for action in actions { if action.canHandle(interpretation) { return true } } return false }
The compound action should override the “canHandle” method and then loop over its collection of actions. It will return “true” only if one of its members can handle the interpretation.
override func handle(interpretation: Interpretation) { for action in actions { if action.canHandle(interpretation) { action.handle(interpretation) return } } defaultFallback(interpretation) }
We will also override the “handle” method in much the same way – loop over each of the contained actions and allow any that “canHandle” the interpretation to act upon it. In the event that none of the actions were able to handle the interpretation, we call a “defaultFallback” method that provides one last opportunity to respond.
func defaultFallback(interpretation: Interpretation) { guard let customFallback = fallback else { if commands.contains(interpretation.command) { LoggingSystem.instance.addLog(BaseAction.invalidSentence) } return } customFallback(interpretation) }
The default fallback method uses a guard statement to see if the CompoundAction had been provided a custom “fallback” block when it was initialized or not. If we have a custom block then this method will invoke it. Otherwise, it will check to see if its commands include the command in the interpretation, and if so, then we log the “invalidSentence” to indicate that although we understand the command, it was phrased in an unexpected or at least unhandled way.
No Target Error Action
This action is a reusable subclass which exists for any action which would expect a target and didn’t receive one. Rather than using a generic “I dont understand” sort of message, I print the question “What do you want to {action}?” – for example if the user merely types “open” then the output could be “What do you want to open?” whereas if the user types “open mailbox” then a different action will be in place to handle it.
import Foundation class NoTargetErrorAction: BaseAction { init(commands: Set<String>) { super.init(commands: commands, specifiers: [], primaryTargetMode: .Zero, secondaryTargetMode: .Zero) } override func handle(interpretation: Interpretation) { let message = "What do you want to \(interpretation.command.lowercaseString)?" LoggingSystem.instance.addLog(message) } }
Openable Actions
Since opening and closing actions are largely the same, I created a shared base class which could handle the filtering and validation, as well as actually executing the action and printing the results:
private class BaseOpenableTargetAction: BaseAction { func customOpenFilter(target: Target) { target.candidates = target.candidates.filter({ (entity) -> Bool in return entity.getOpenable() != nil }) if target.candidates.count == 0 { target.error = "You must tell me how to do that to a \(target.userInput)." return } } func tryAction(target: Target, action: (Openable)->(String)) { TargetingSystem.filter(target, options: [TargetingFilter.CurrentRoom, TargetingFilter.ContainerIsOpen]) customOpenFilter(target) TargetingSystem.validate(target) guard let match = target.match, openable = match.getOpenable() where target.error == nil else { LoggingSystem.instance.addLog(target.error!) return } let message = action(openable) LoggingSystem.instance.addLog(message) } }
The “tryAction” method will be invoked in the “handle” method of the open and close subclassed actions. It will first use the Targeting system to filter the candidates such that anything we wish to open or close will be located within the same room as the player, and also that if it is located within another openable container that the container is also open. An openable within an openable could occur if you were to stick the sack in the case or the bottle in the mailbox, etc. It will then use a custom filter which verifies that the entity we wish to open actually has an openable component attached. Finally the target is run through the targeting systems validate method to ensure we have found a match. If so, we attempt to use the relevant block (open or close), otherwise we log whatever targeting error we have encountered.
private class OpenTargetAction: BaseOpenableTargetAction { override private func handle(interpretation: Interpretation) { guard let target = interpretation.primary.first else { return } tryAction(target, action: OpenableSystem.open) } } private class CloseTargetAction: BaseOpenableTargetAction { override private func handle(interpretation: Interpretation) { guard let target = interpretation.primary.first else { return } tryAction(target, action: OpenableSystem.close) } }
Here are the subclasses for the “BaseOpenableTargetAction” – one for “close” and one for “open”. When one of these actions are invoked they will try to call the relevant method on the Openable System.
class OpenAction: CompoundAction { init() { let noTarget = NoTargetErrorAction(commands: ["OPEN"]) let target = OpenTargetAction(commands: ["OPEN"], specifiers: [], primaryTargetMode: .Single, secondaryTargetMode: .Zero) super.init(actions: [noTarget, target]) } } class CloseAction: CompoundAction { init() { let noTarget = NoTargetErrorAction(commands: ["CLOSE"]) let target = CloseTargetAction(commands: ["CLOSE"], specifiers: [], primaryTargetMode: .Single, secondaryTargetMode: .Zero) super.init(actions: [noTarget, target]) } }
Why another set of open and close actions? These two compound actions are the ones which are actually registered to the interpreter. They include a “NoTargetErrorAction” as well as the relevant “Open/CloseTargetAction” so that it is easy to handle either command regardless of if a target is provided. Unless there is a good reason not to, I will probably always use a subclass of the compound action when registering with the interpreter because it makes it so easy to add additional cases of a command to handle.
Demo
Open the “GameViewController” for editing. In the “viewDidLoad” method we can replace the line for registering with the Interpreter with a line that tells the Master system to perform its setup instead:
// Remove this InterpreterSystem.instance.register(self) // Add this MasterSystem.setup()
You can also delete the entire UserAction extension that we were using for our demo. From now on, the output to the screen will come from actual game code.
Save and then Build and Run the project. Experiement with all of the different scenarios for Opening and Closing an entity that you can think of: open or close, open while already open, open something that isn’t openable, or open something you can’t currently see, etc.
Summary
In this lesson we continued to expand our code with more models and systems that help describe and interact with the content from the database. We made a couple of reusable classes to help speed up the implementation of User Actions, and then implemented a few which handle the opening or closing of an entity. This also meant that we actually modified data in the database for the first time. We’ve been creating a game all along, but it is really starting to feel like one now.
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.