Now that we can take items, it is time to enable more specific interaction with them. Some stuff you can read, others you can eat or drink. If the interaction includes a trigger, then it should be exectued. We will implement each of these interactions in this lesson, and then to finish it off we will add the ability for the player to persist their game state. Yep, they need to be able to save and load their games.
Triggers
The first interaction I want to add is the trigger. We can open the window on the back of the house, but that action is supposed to modify the portals in the adjacent rooms. Until this happens we cant actually get into the house.
ValueChanger
Add the following struct to wrap the ValueChanger table in our database. A value changer holds the data necessary to change one field of a component data struct. These will be used as components on Trigger entities that are targeted by some action.
import Foundation import SQLite struct ValueChanger { // MARK: - Fields static let component_id: Int64 = 9 static let table = Table("ValueChangers") static let value_changer_id_column = Expression<Int64>("id") static let component_id_column = Expression<Int64>("component_id") static let component_data_id_column = Expression<Int64>("component_data_id") static let column_name_column = Expression<String>("column_name") static let change_int_column = Expression<Int64?>("change_int") static let change_text_column = Expression<String?>("change_text") let row: Row // MARK: - Properties var id: Int64 { get { return row[ValueChanger.value_changer_id_column] } } var componentID: Int64 { get { return row[ValueChanger.component_id_column] } } var componentDataID: Int64 { get { return row[ValueChanger.component_data_id_column] } } var columnName: String { get { return row[ValueChanger.column_name_column] } } var changeInt: Int64? { get { return row[ValueChanger.change_int_column] } } var changeText: String? { get { return row[ValueChanger.change_text_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(ValueChanger.component_id, dataID: id) } } } extension Entity { func getValueChangers() -> [ValueChanger] { let components = getComponents(ValueChanger.component_id) let ids = components.map({ $0.dataID }) let table = ValueChanger.table.filter(ids.contains(ValueChanger.value_changer_id_column)) let result = DataManager.instance.prepare(table).map({ ValueChanger(row: $0) }) return result } }
Trigger System
Next we will add a system that actually applies the changes specified in a ValueChanger component.
import Foundation import SQLite class TriggerSystem { class func execute(trigger: Entity?) { guard let trigger = trigger else { return } let valueChangers = trigger.getValueChangers() for vc in valueChangers { apply(vc) } } class func apply(valueChanger: ValueChanger) { guard let component = Component.fetchByID(valueChanger.componentID) else { return } let idExpression = Expression<Int64>("id") let componentTable = Table(component.tableName) var update: Update? if let changeInt = valueChanger.changeInt { let columnExpression = Expression<Int64>(valueChanger.columnName) update = componentTable.filter(idExpression == valueChanger.componentDataID).update(columnExpression <- changeInt) } else if let changeText = valueChanger.changeText { let columnExpression = Expression<String>(valueChanger.columnName) update = componentTable.filter(idExpression == valueChanger.componentDataID).update(columnExpression <- changeText) } if let update = update { DataManager.instance.run(update) } } }
Any system obtaining a reference to a trigger can call the “execute” method here. The system will then iterate over all of the attached ValueChanger components and apply their changes.
The apply method is able to update the database using the information provided. In most other classes I used a predefined static field as an expression, but this method shows that a dynamic expression works equally as well.
Openable System
Currently, the openable system is the only implementation I have for working with a trigger. In the “open” method just before the final return statement, add the following:
TriggerSystem.execute(item.openTrigger)
In the “close” statement, just before the final return statement, add the following:
TriggerSystem.execute(item.closeTrigger)
Reading
Next let’s add the ability to read an entity which contains a “Readable” component. This will allow you to read the message on the leaflet after taking it from the mailbox.
Readable
Add the following struct to wrap the Readable table in our database. A readable could be anyhing with content to read such as a book or leaflet.
import Foundation import SQLite struct Readable { // MARK: - Fields static let component_id: Int64 = 10 static let table = Table("Readables") static let readable_id_column = Expression<Int64>("id") static let content_column = Expression<String?>("content") let row: Row // MARK: - Properties var id: Int64 { get { return row[Readable.readable_id_column] } } var content: String? { get { return row[Readable.content_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(Readable.component_id, dataID: id) } } } extension Entity { func getReadable() -> Readable? { guard let component = getComponent(Readable.component_id) else { return .None } let table = Readable.table.filter(Readable.readable_id_column == component.dataID) guard let result = DataManager.instance.prepare(table).first else { return .None } return Readable(row: result) } }
Readable System
Systems don’t get much easier than this. Assuming the Read Action was validated, all you need to do is print the content field or a generic message when no content was found.
import Foundation class ReadableSystem { class func setup() { InterpreterSystem.instance.register(ReadAction()) } class func read(readable: Readable) -> String { guard let content = readable.content else { return "I can't read that." } return content } }
Read Action
This action follows the patterns we have used before. A compound action is registered to the interpreter, which is made up of a combination of other actions. When no target is provided we show an error, if an unreadable target is provided, it gets filtered out with a message, and if a readable target is provided we trigger the appropriate message in the Readable System.
import Foundation class ReadAction: CompoundAction { init() { let noTarget = NoTargetErrorAction(commands: ["READ"]) let target = ReadTargetAction(commands: ["READ"], specifiers: [], primaryTargetMode: .Single, secondaryTargetMode: .Zero) super.init(actions: [noTarget, target]) } } class ReadTargetAction: BaseAction { private func customReadFilter(target: Target) { target.candidates = target.candidates.filter({ (entity) -> Bool in return entity.getReadable() != nil }) if target.candidates.count == 0 { target.error = "How does one read a \(target.userInput)?" return } } override func handle(interpretation: Interpretation) { guard let target = interpretation.primary.first else { return } TargetingSystem.filter(target, options: [TargetingFilter.CurrentRoom, TargetingFilter.ContainerIsOpen]) customReadFilter(target) TargetingSystem.filter(target, options: [TargetingFilter.HeldByPlayer]) TargetingSystem.validate(target) guard let match = target.match, readable = match.getReadable() where target.error == .None else { guard let error = target.error else { return } LoggingSystem.instance.addLog(error) return } let message = ReadableSystem.read(readable) LoggingSystem.instance.addLog(message) } }
Eating & Drinking
I’m not sure if this serves a real purpose in Zork or not, but it was there so I included it too. You can find a sandwich and garlic to eat if you look in the sack on the kitchen table. You can also find a bottle of water to drink. Eating or drinking an entity is a destructive action – after consuming an entity I will move it to an invalid room so you wont be able to find it again. I could also simply delete the entity and its components from the database if desired. Just be careful in that case that you dont delete shared components such as the nouns and adjectives.
Eatable
Add the following struct to wrap the Eatable table in our database. This component is attached to something which can be eaten.
import Foundation import SQLite struct Eatable { // MARK: - Fields static let component_id: Int64 = 12 static let table = Table("Eatables") static let eatable_id_column = Expression<Int64>("id") static let message_column = Expression<String?>("message") let row: Row // MARK: - Properties var id: Int64 { get { return row[Eatable.eatable_id_column] } } var message: String? { get { return row[Eatable.message_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(Eatable.component_id, dataID: id) } } } extension Entity { func getEatable() -> Eatable? { guard let component = getComponent(Eatable.component_id) else { return .None } let table = Eatable.table.filter(Eatable.eatable_id_column == component.dataID) guard let result = DataManager.instance.prepare(table).first else { return .None } return Eatable(row: result) } }
Drinkable
Add the following struct to wrap the Drinkable table in our database. This component is attached to something which one can drink.
import Foundation import SQLite struct Drinkable { // MARK: - Fields static let component_id: Int64 = 13 static let table = Table("Drinkables") static let drinkable_id_column = Expression<Int64>("id") static let message_column = Expression<String?>("message") let row: Row // MARK: - Properties var id: Int64 { get { return row[Drinkable.drinkable_id_column] } } var message: String? { get { return row[Drinkable.message_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(Drinkable.component_id, dataID: id) } } } extension Entity { func getDrinkable() -> Drinkable? { guard let component = getComponent(Drinkable.component_id) else { return .None } let table = Drinkable.table.filter(Drinkable.drinkable_id_column == component.dataID) guard let result = DataManager.instance.prepare(table).first else { return .None } return Drinkable(row: result) } }
Eatable & Drinkable Systems
Here are some more easy systems. Eating or drinking an entity simply moves its container reference to an invalid entity id of ‘0’ so that you wont see it again. Then we follow up with a relevant message. This might be a great opportunity for triggers in the future. Perhaps if you were making Alice in Wonderland, then eating an item might cause you to grow or shrink. Many games take the opportunity to restore hitpoints after eating food.
import Foundation class EatableSystem { class func setup() { InterpreterSystem.instance.register(EatAction()) } class func eat(eatable: Eatable) -> String { if let containable = eatable.entity?.getContainable() { ContainmentSystem.move(containable, containingEntityID: 0) } let message = eatable.message ?? "Eaten." return message } }
Likewise you could reserve drinking for the effects of potions. Perhaps you get smarter or are temporarily faster or stronger, etc. Lots of potential here!
import Foundation class DrinkableSystem { class func setup() { InterpreterSystem.instance.register(DrinkAction()) } class func drink(drinkable: Drinkable) -> String { if let containable = drinkable.entity?.getContainable() { ContainmentSystem.move(containable, containingEntityID: 0) } let message = drinkable.message ?? "Done." return message } }
Eat & Drink Actions
Again there isnt much new here. We need to filter and validate the candidates. For items we wish to consume we add the filter that they contain the relevant component and that they are currently held by the player. Valid commands are passed along to the relevant system for execution of the action.
import Foundation class EatAction: CompoundAction { init() { let noTarget = NoTargetErrorAction(commands: ["EAT"]) let target = EatTargetAction(commands: ["EAT"], specifiers: [], primaryTargetMode: .Single, secondaryTargetMode: .Zero) super.init(actions: [noTarget, target]) } } private class EatTargetAction: BaseAction { private func customEatFilter(target: Target) { target.candidates = target.candidates.filter({ (entity) -> Bool in return entity.getEatable() != nil }) if target.candidates.count == 0 { target.error = "I don't think that the \(target.userInput) would agree with you." return } } override func handle(interpretation: Interpretation) { guard let target = interpretation.primary.first else { return } TargetingSystem.filter(target, options: [TargetingFilter.CurrentRoom, TargetingFilter.ContainerIsOpen]) customEatFilter(target) TargetingSystem.filter(target, options: TargetingFilter.HeldByPlayer) TargetingSystem.validate(target) guard let match = target.match, eatable = match.getEatable() where target.error == .None else { guard let error = target.error else { return } LoggingSystem.instance.addLog(error) return } let message = EatableSystem.eat(eatable) LoggingSystem.instance.addLog(message) } }
The drink action is nearly identical to the eat action. It probably wouldn’t hurt to refactor them to share a base class, but I have left them separate for now just in case I want to implement something a little more unique and relevant to each.
import Foundation class DrinkAction: CompoundAction { init() { let noTarget = NoTargetErrorAction(commands: ["DRINK"]) let target = DrinkTargetAction(commands: ["DRINK"], specifiers: [], primaryTargetMode: .Single, secondaryTargetMode: .Zero) super.init(actions: [noTarget, target]) } } private class DrinkTargetAction: BaseAction { private func customDrinkFilter(target: Target) { target.candidates = target.candidates.filter({ (entity) -> Bool in return entity.getDrinkable() != nil }) if target.candidates.count == 0 { target.error = "I don't think that the \(target.userInput) would agree with you." return } } override func handle(interpretation: Interpretation) { guard let target = interpretation.primary.first else { return } TargetingSystem.filter(target, options: [TargetingFilter.CurrentRoom, TargetingFilter.ContainerIsOpen]) customDrinkFilter(target) TargetingSystem.filter(target, options: TargetingFilter.HeldByPlayer) TargetingSystem.validate(target) guard let match = target.match, drinkable = match.getDrinkable() where target.error == .None else { guard let error = target.error else { return } LoggingSystem.instance.addLog(error) return } let message = DrinkableSystem.drink(drinkable) LoggingSystem.instance.addLog(message) } }
Persistence
We’ve actually had the ability to save and load or start a new game almost since the beginning of the project. I simply didnt create a user command that triggers it. Let’s go ahead and do that now so that our progress while playing isn’t lost.
Game System
import Foundation class GameSystem { class func setup() { InterpreterSystem.instance.register(SaveGameAction()) InterpreterSystem.instance.register(LoadGameAction()) InterpreterSystem.instance.register(NewGameAction()) load() } class func load() { DataManager.instance.load() RoomSystem.move(RoomSystem.room.id) } class func save() { DataManager.instance.save() } class func newGame() { DataManager.instance.reset() RoomSystem.move(RoomSystem.room.id) } }
This system registers the commands a user will need in order to trigger the saving, loading, or resetting of the game. It also automatically moves the player into the starting room so that we dont start the game with a blank screen – we will at least have the context of our current location.
Save Game Action
Again, I have used a compound action even though I only used a single action in the actions array. I could see a reason to extend this in the future so that you could do something like “save as” and provide a name for your save file.
import Foundation class SaveGameAction: CompoundAction { init() { let action = BaseSaveGameAction(commands: ["SAVE"], specifiers: [], primaryTargetMode: .Zero, secondaryTargetMode: .Zero) super.init(actions: [action]) } } private class BaseSaveGameAction: BaseAction { override func handle(interpretation: Interpretation) { GameSystem.save() LoggingSystem.instance.addLog("Saved.") } }
Load Game Action
Just like with saving, in the future I might want to be able to specify a particular file to load. I went ahead and made this a compound action as well.
import Foundation class LoadGameAction: CompoundAction { init() { let action = BaseLoadGameAction(commands: ["LOAD"], specifiers: [], primaryTargetMode: .Zero, secondaryTargetMode: .Zero) super.init(actions: [action]) } } private class BaseLoadGameAction: BaseAction { override func handle(interpretation: Interpretation) { LoggingSystem.instance.addLog("Loading...") GameSystem.load() } }
New Game Action
I may not have any reason to name a file in regards to starting a new game, but I could see using synonyms here such as “reset” or even allowing the appended word “game” just to be more specific. Go ahead and make it a compound action!
import Foundation class NewGameAction: CompoundAction { init() { let action = BaseNewGameAction(commands: ["NEW"], specifiers: [], primaryTargetMode: .Zero, secondaryTargetMode: .Zero) super.init(actions: [action]) } } private class BaseNewGameAction: BaseAction { override private func handle(interpretation: Interpretation) { LoggingSystem.instance.addLog("Beginning new game...") GameSystem.newGame() } }
Master System
We have now provided all of the game systems I created for this prototype. The final master system setup method is as follows:
class func setup() { GameSystem.setup() SightSystem.setup() PortalSystem.setup() OpenableSystem.setup() PlayerSystem.setup() TakeableSystem.setup() ReadableSystem.setup() EatableSystem.setup() DrinkableSystem.setup() }
Game View Controller
A few more minor changes and we are done! In the “viewDidLoad” method, remove the statement which loads the DataManager because this now occurs in the GameSystem. Also, after calling setup on the master system let’s tell the Logging System to print to the screen – there will actually be some initial content to read.
override func viewDidLoad() { super.viewDidLoad() MasterSystem.setup() historyTextView.text = LoggingSystem.instance.print() }
Demo
Build and run the project. You should be able to reach any room now, as well as fully experience all of the content I put into the database. Did you find my Matrix reference in the game? It wasn’t part of the original Zork, but I needed to verify the proper error message would appear when you had more than one candidate after filtering and validating a command, so I snuck it in there somewhere. (hint – it has to do with pills)
Summary
In this lesson we finished out the interaction with components. We can now use triggers as the result of an interaction, and we also added new ways to interact such as by reading, eating and drinking. We also added some commands so that we could activate the Datamanager’s ability to save and load our progress.
That’s it, we’re done with this project. There is a ton you could add, but I feel we have laid a great foundation from which you should be able to do just about whatever you want.
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.