Our interpreter system has succeeded in converting words to actions and objects. However it is still only providing candidate matches. There are a variety of reasons why we may need to filter or disregard those results based on other game constraints. For example, even though the interpreter might understand the word “leaflet”, you still shouldn’t consider it a valid object to interact with unless your player can actually see it. It could be in a different room, or could be located in a closed mailbox, etc. In this lesson, we will provide a way to help validate our object targets.
Models
In order to complete this lesson we will need to implement a few more models. They are 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. The only notable difference in these is that I have added an “entity” property which allows you to have an inverse relationship from a component back to the entity that it is located on.
Room
A room is like a scene of conent. It has a title and a description which appear when navigating between rooms. A boolean keeps track of whether or not you have visited any given room before. When returning to a room, only the title will be shown to help keep the screen a little less cluttered.
import Foundation import SQLite struct Room { // MARK: - Fields static let component_id: Int64 = 1 static let table = Table("Rooms") static let room_id_column = Expression<Int64>("id") static let title_column = Expression<String>("title") static let description_column = Expression<String>("description") static let visited_column = Expression<Bool>("visited") let row: Row // MARK: - Properties var id: Int64 { get { return row[Room.room_id_column] } } var title: String { get { return row[Room.title_column] } } var description: String { get { return row[Room.description_column] } } var visited: Bool { get { return row[Room.visited_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(Room.component_id, dataID: id) } } } extension Entity { func getRoom() -> Room? { guard let component = getComponent(Room.component_id) else { return .None } let table = Room.table.filter(Room.room_id_column == component.dataID) guard let result = DataManager.instance.prepare(table).first else { return .None } return Room(row: result) } }
Presence
This component indicates that the location of an entity is best described by its presence in a room(s). Some entities will appear in only one room like the player or mailbox. Other entities like a window or door exist on a room border and will appear in two rooms. Some generic entities like the ground can appear in almost every room.
import Foundation import SQLite struct Presence { // MARK: - Fields static let component_id: Int64 = 14 static let table = Table("Presences") static let presence_id_column = Expression<Int64>("id") static let room_id_column = Expression<Int64>("room_id") let row: Row // MARK: - Properties var id: Int64 { get { return row[Presence.presence_id_column] } } var roomID: Int64 { get { return row[Presence.room_id_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(Presence.component_id, dataID: id) } } } extension Entity { func getPresences() -> [Presence] { let components = getComponents(Presence.component_id) let ids = components.map({ $0.dataID }) let table = Presence.table.filter(ids.contains(Presence.presence_id_column)) let result = DataManager.instance.prepare(table).map({ Presence(row: $0) }) return result } }
Containable
This component indicates that the location of an entity is best described in relation to another entity. For example, the leaflet is held inside the mailbox, or a dagger might be held by the player (in his inventory). The containing entity can be a room, but will usually be an entity with a container component.
import Foundation import SQLite struct Containable { // MARK: - Fields static let component_id: Int64 = 4 static let table = Table("Containables") static let containable_id_column = Expression<Int64>("id") static let containing_entity_id_column = Expression<Int64>("containing_entity_id") static let size_column = Expression<Double>("size") let row: Row // MARK: - Properties var id: Int64 { get { return row[Containable.containable_id_column] } } var containingEntityID: Int64 { get { return row[Containable.containing_entity_id_column] } set(newValue) { } } var containingEntity: Entity? { get { return Entity.fetchByID(self.containingEntityID) } } var size: Double { get { return row[Containable.size_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(Containable.component_id, dataID: id) } } } extension Entity { func getContainable() -> Containable? { guard let component = getComponent(Containable.component_id) else { return .None } let table = Containable.table.filter(Containable.containable_id_column == component.dataID) guard let result = DataManager.instance.prepare(table).first else { return .None } return Containable(row: result) } }
Openable
An openable could be anything that has a concept of opening, such as opening a window, door, or mailbox. Opening and Closing an openable can have triggers such as enabling or disabling another component or changing another component’s values.
import Foundation import SQLite struct Openable { // MARK: - Fields static let component_id: Int64 = 7 static let table = Table("Openables") static let openable_id_column = Expression<Int64>("id") static let is_open_column = Expression<Bool>("is_open") static let is_locked_column = Expression<Bool>("is_locked") static let open_message_column = Expression<String?>("open_message") static let open_trigger_column = Expression<Int64?>("open_trigger_id") static let close_message_column = Expression<String?>("close_message") static let close_trigger_column = Expression<Int64?>("close_trigger_id") let row: Row // MARK: - Properties var id: Int64 { get { return row[Openable.openable_id_column] } } var isOpen: Bool { get { return row[Openable.is_open_column] } } var isLocked: Bool { get { return row[Openable.is_locked_column] } } var openMessage: String? { get { return row[Openable.open_message_column] } } var openTriggerID: Int64? { get { return row[Openable.open_trigger_column] } } var openTrigger: Entity? { get { guard let triggerID = openTriggerID else { return .None } return Entity.fetchByID(triggerID) } } var closeMessage: String? { get { return row[Openable.close_message_column] } } var closeTriggerID: Int64? { get { return row[Openable.close_trigger_column] } } var closeTrigger: Entity? { get { guard let triggerID = closeTriggerID else { return .None } return Entity.fetchByID(triggerID) } } var entity: Entity? { get { return Entity.fetchByComponentID(Openable.component_id, dataID: id) } } } extension Entity { func getOpenable() -> Openable? { guard let component = getComponent(Openable.component_id) else { return .None } let table = Openable.table.filter(Openable.openable_id_column == component.dataID) guard let result = DataManager.instance.prepare(table).first else { return .None } return Openable(row: result) } }
Notable
Marks an item which should be described upon entering a room. It may also be used to help describe an entity in favor of a more generic combination of its adjectives and nouns.
import Foundation import SQLite struct Notable { // MARK: - Fields static let component_id: Int64 = 5 static let table = Table("Notables") static let notable_id_column = Expression<Int64>("id") static let description_column = Expression<String?>("description") let row: Row // MARK: - Properties var id: Int64 { get { return row[Notable.notable_id_column] } } var description: String? { get { return row[Notable.description_column] } } var entity: Entity? { get { return Entity.fetchByComponentID(Notable.component_id, dataID: id) } } } extension Entity { func getNotable() -> Notable? { guard let component = getComponent(Notable.component_id) else { return .None } let table = Notable.table.filter(Notable.notable_id_column == component.dataID) guard let result = DataManager.instance.prepare(table).first else { return .None } return Notable(row: result) } }
Targeting Filter
Unlike the previous models, this structure does not represent a table in our database. It is a special kind of structure that works like bit-masks in other languages. Basically this means that I can turn on any of a combination of flags with a single property. Each of the four flags I have provided represent one of the possible ways we will potentially filter down and validate the entities in our candidate lists.
import Foundation public struct TargetingFilter : OptionSetType{ public let rawValue : Int public init(rawValue:Int){ self.rawValue = rawValue} // Require that all candidates appear in the room that the player is located within static let CurrentRoom = TargetingFilter(rawValue: 1 << 2) // Require that any candidate contained by an openable container has the container open static let ContainerIsOpen = TargetingFilter(rawValue: 1 << 3) // Require that all candidates be in the player's inventory (or be contained by an object therein) static let HeldByPlayer = TargetingFilter(rawValue: 1 << 4) // Require that all candidates NOT be in the player's inventory (or be contained by an object therein) static let NotHeldByPlayer = TargetingFilter(rawValue: 1 << 5) }
Systems
In proper ECS style, the methods for our models will be added to some new systems:
ContainmentSystem
This system will eventually handle a variety of methods for working with containable and container components such as determining whether or not a container has enough capacity to hold a containable, or to grab the list of entities contained by a container, etc. For this lesson all we need is the ability to determine the root entity of a containable object. This means that you follow the container hierarchy of the containable entity until you reach an entity which is not contained by anything else.
import Foundation import SQLite class ContainmentSystem { class func rootEntity(containable: Containable) -> Entity? { guard let containingEntity = containable.containingEntity else { return containable.entity } guard let parentContainable = containingEntity.getContainable() else { return containingEntity } return rootEntity(parentContainable) } }
Player System
This system provides convenient access to the entity which is represented by the player. It also provides a convenience method to determine whether or not a particular containable entity is held by the player – this would be the player’s inventory. We can add to this system later as well, but for now this is all we need.
import Foundation class PlayerSystem { static var player: Entity { get { guard let entity = Entity.fetchByID(1) else { fatalError("Can't locate the Player") } return entity } } class func heldByPlayer(containable: Containable?) -> Bool { guard let containable = containable else { return false } guard let rootEntity = ContainmentSystem.rootEntity(containable) else { return false } return rootEntity.id == player.id } }
Room System
This system provides convenient access to the current room (the room the player is located within). It allows the ability to fetch rooms by a room id, and provides a convenience method to determine whether or not a given entity is present within it. This method actually checks for the presence of an entity by the containable’s root entity as well as by a presence component. Some entities, like the leaflet, dont include a presence component but do include a containable component, so the ability to check by either means is important here.
import Foundation import SQLite class RoomSystem { static var room: Room { get { guard let roomID = PlayerSystem.player.getPresences().first?.roomID, result = RoomSystem.fetchByID(roomID) else { fatalError("The player must appear in a room") } return result } } class func fetchByID(id: Int64) -> Room? { let table = Room.table.filter(Room.room_id_column == id) guard let result = DataManager.instance.prepare(table).first else { return .None } return Room(row: result) } class func isPresentInCurrentRoom(entity: Entity) -> Bool { if let containable = entity.getContainable(), roomEntityID = RoomSystem.room.entity?.id, containerEntity = ContainmentSystem.rootEntity(containable) { if containerEntity.id == roomEntityID { return true } if let _ = containerEntity.getPresences().filter({ $0.roomID == room.id }).first { return true } return false } if let _ = entity.getPresences().filter({ $0.roomID == room.id }).first { return true } return false } }
Label System
Open the label system and append the following method which will allow us to describe a particular entity:
class func describe(entity: Entity) -> String { if let description = entity.getNotable()?.description { return description } if let noun = entity.getNouns().first { let adjectives = entity.getAdjectives().map({ $0.text }).joinWithSeparator(" ") return adjectives.characters.count > 0 ? "\(adjectives) \(noun.text)" : noun.text } else { fatalError("Cannot describe an entity without a notable or noun component") } }
Targeting System
Now that the setup work is done we can finally dive in to the real purpose for this chapter. The targeting system will use several of the attributes we now know about our game entities to filter down a list of candidates and validate them based on any combination of requirements you desire.
import Foundation class TargetingSystem { class func filter(target: Target, options: TargetingFilter) { if target.candidates.count == 0 { target.error = "I don't know what a \(target.userInput) is." return } if options.contains(.CurrentRoom) { target.candidates = target.candidates.filter({ RoomSystem.isPresentInCurrentRoom($0) }) if target.candidates.count == 0 { target.error = "You can't see any \(target.userInput) here." return } } if options.contains(.ContainerIsOpen) { target.candidates = target.candidates.filter({ (entity) -> Bool in if let container = entity.getContainable()?.containingEntity?.getOpenable() { return container.isOpen } return true }) if target.candidates.count == 0 { target.error = "You can't see any \(target.userInput) here." return } } if options.contains(.HeldByPlayer) { target.candidates = target.candidates.filter({ (entity) -> Bool in return PlayerSystem.heldByPlayer(entity.getContainable()) }) if target.candidates.count == 0 { target.error = "You don't have the \(target.userInput)." } } if options.contains(.NotHeldByPlayer) { target.candidates = target.candidates.filter({ (entity) -> Bool in return !PlayerSystem.heldByPlayer(entity.getContainable()) }) if target.candidates.count == 0 { target.error = "You already have the \(target.userInput)!" return } } } // TODO: Add code here }
The “filter” method takes a target (which contains the list of candidate entities), and an options filter which allows us to specifiy “how” we want to filter down the list of candidates. The goal is to reduce the list of candidates until there is exactly one candidate left, but note that even if we did have only a single candidate that we still need to validate it as well. For example, knowing that there is only one entity which can represent the word “leaflet” isn’t enough to try to read it when you aren’t holding it.
This method can potentially be called iteratively, or all in one fell swoop. I tried to put the checks in the most logical order such that the first error message to be applied would be the one that is most important to resolve.
Initially we perform a check to see if there are any candidates at all for a given target. If not, then we can abort early due to not knowing what the target is. Next, there are conditional cases that will filter down the list but only if that flag is set on the filter options. Any time a filter is run and the candidates list is reduced to zero, we can abort early.
class func validate(target: Target) { guard target.error == .None else { return } switch target.candidates.count { case 0: target.error = "You can't see any \(target.userInput) here." break case 1: target.match = target.candidates.first break default: let candidateDescriptions = target.candidates.map({ LabelSystem.describe($0) }).joinWithSeparator(" or ") target.error = "Do you mean the \(candidateDescriptions)?" break } }
After we have run all of the filters we intend on a given target, we can then validate it. This final step simply verifies that we have successfully reduced the candidate list down to exactly one option. Otherwise, we update the target with an appropriate error.
Demo
Open the GameViewController and find the demo code from the previous lesson. Insert the following at the end of the “handle” method:
guard let target = interpretation.primary.first else { return } TargetingSystem.filter(target, options: [TargetingFilter.CurrentRoom, TargetingFilter.ContainerIsOpen, TargetingFilter.NotHeldByPlayer]) if let error = target.error { LoggingSystem.instance.addLog("validation Error: \(error)") } else { LoggingSystem.instance.addLog("validation Success!") }
Go ahead and save, then build and run the project. Assuming that the sentence you type in has a target, this code will attempt to validate it by whatever filters you enable in the options mask. Below is an example image of me attempting to “open” two different entities, one of which I know exists in player’s starting room (the mailbox), and one which doesn’t (the window).
Feel free to modify the options to a variety of different combinations, although until we enable you to actually navigate to a new room and interact with objects, you will still be a bit limited.
Summary
In this lesson we added some models and systems which helped provide a little more knowledge about the entities which make up our game world. With that new knowledge we were able to provide another system that could help filter and validate our interpreter’s target candidates lists so that our actions can better decide what to interact with and whether or not to allow the interaction.
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.