diff --git a/Sources/FirebladeECS/FSM.swift b/Sources/FirebladeECS/FSM.swift new file mode 100644 index 0000000..c1ea41b --- /dev/null +++ b/Sources/FirebladeECS/FSM.swift @@ -0,0 +1,167 @@ +public protocol EmptyInitializable { + init() +} + +public typealias ComponentInitializable = Component & EmptyInitializable + +public protocol ComponentProvider { + var identifier: AnyHashable { get } + func getComponent() -> C? where C: Component +} + +// MARK: - + +public struct ComponentInstanceProvider { + private var instance: Component + + public init(instance: Component) { + self.instance = instance + } +} + +extension ComponentInstanceProvider: ComponentProvider { + public var identifier: AnyHashable { + ObjectIdentifier(instance) + } + + public func getComponent() -> C? where C : Component { + instance as? C + } +} + +// MARK: - + +public struct ComponentTypeProvider { + private var componentType: ComponentInitializable.Type + public let identifier: AnyHashable + + public init(type: T.Type) { + componentType = type + identifier = ObjectIdentifier(componentType.self) + } +} + +extension ComponentTypeProvider: ComponentProvider { + public func getComponent() -> C? where C: Component { + componentType.init() as? C + } +} + + +// MARK: - + +public class ComponentSingletonProvider { + lazy private var instance: Component = { + componentType.init() + }() + private var componentType: ComponentInitializable.Type + + public var identifier: AnyHashable { + ObjectIdentifier(instance) + } + + public init(type: T.Type) { + componentType = type + } + + internal init(type: ComponentInitializable.Type) { + componentType = type + } +} + +extension ComponentSingletonProvider: ComponentProvider { + public func getComponent() -> C? where C: Component { + instance as? C + } +} + +// MARK: - + +public struct DynamicComponentProvider { + public class Closure { + let closure: () -> Component + public init(closure: @escaping () -> Component) { + self.closure = closure + } + } + private let closure: Closure + + public init(closure: Closure) { + self.closure = closure + } +} + +extension DynamicComponentProvider: ComponentProvider { + public var identifier: AnyHashable { + ObjectIdentifier(closure) + } + + public func getComponent() -> C? where C: Component { + closure.closure() as? C + } +} + +// MARK: - + +public class EntityState { + internal var providers = [ComponentIdentifier: ComponentProvider]() + + public init() { } + + public func add(_ type: C.Type) -> StateComponentMapping { + StateComponentMapping(creatingState: self, + type: type) + } +} +// MARK: - + +public class StateComponentMapping { + private var componentType: ComponentInitializable.Type + private let creatingState: EntityState + private var provider: ComponentProvider + + public init(creatingState: EntityState, type: T.Type) { + self.creatingState = creatingState + componentType = type + provider = ComponentTypeProvider(type: type) + } + + public func withInstance(_ component: Component) -> StateComponentMapping { + setProvider(ComponentInstanceProvider(instance: component)) + return self + } + + public func withType(_ type: T.Type) -> Self { + setProvider(ComponentTypeProvider(type: type)) + return self + } + + public func withSingleton(_ type: T.Type?) -> Self { + if let type = type { + setProvider(ComponentSingletonProvider(type: type)) + } else { + setProvider(ComponentSingletonProvider(type: componentType)) + } + + return self + } + + public func withMethod(_ closure: DynamicComponentProvider.Closure) -> Self { + setProvider(DynamicComponentProvider(closure: closure)) + return self + } + + public func withProvider(_ provider: ComponentProvider) -> Self { + setProvider(provider) + return self + } + + public func add(_ type: T.Type) -> StateComponentMapping { + creatingState.add(type) + } + + private func setProvider(_ provider: ComponentProvider) { + self.provider = provider + creatingState.providers[componentType.identifier] = provider + } +} diff --git a/Tests/FirebladeECSTests/FSMTests.swift b/Tests/FirebladeECSTests/FSMTests.swift new file mode 100644 index 0000000..aeeda68 --- /dev/null +++ b/Tests/FirebladeECSTests/FSMTests.swift @@ -0,0 +1,155 @@ +import FirebladeECS +import XCTest + +class ComponentInstanceProviderTests: XCTestCase { + func testProviderReturnsTheInstance() { + let instance = MockComponent(value: .max) + let provider1 = ComponentInstanceProvider(instance: instance) + let providedComponent: MockComponent? = provider1.getComponent() + XCTAssertTrue(providedComponent === instance) + } + + func testProvidersWithSameInstanceHaveSameIdentifier() { + let instance = MockComponent(value: .max) + let provider1 = ComponentInstanceProvider(instance: instance) + let provider2 = ComponentInstanceProvider(instance: instance) + XCTAssertEqual(provider1.identifier, provider2.identifier) + } + + func testProvidersWithDifferentInstanceHaveDifferentIdentifier() { + let provider1 = ComponentInstanceProvider(instance: MockComponent(value: .max)) + let provider2 = ComponentInstanceProvider(instance: MockComponent(value: .max)) + XCTAssertNotEqual(provider1.identifier, provider2.identifier) + } + + class MockComponent: Component { + var value: Int + + init(value: Int) { + self.value = value + } + } +} + +class ComponentTypeProviderTests: XCTestCase { + func testProviderReturnsAnInstanceOfType() { + let provider = ComponentTypeProvider(type: MockComponent.self) + let component: MockComponent? = provider.getComponent() + XCTAssertNotNil(component) + } + + func testProviderReturnsNewInstanceEachTime() { + let provider = ComponentTypeProvider(type: MockComponent.self) + let component1: MockComponent? = provider.getComponent() + let component2: MockComponent? = provider.getComponent() + XCTAssertFalse(component1 === component2) + } + + func testProvidersWithSameTypeHaveSameIdentifier() { + let provider1 = ComponentTypeProvider(type: MockComponent.self) + let provider2 = ComponentTypeProvider(type: MockComponent.self) + XCTAssertEqual(provider1.identifier, provider2.identifier) + } + + func testProvidersWithDifferentTypeHaveDifferentIdentifier() { + let provider1 = ComponentTypeProvider(type: MockComponent.self) + let provider2 = ComponentTypeProvider(type: MockComponent2.self) + XCTAssertNotEqual(provider1.identifier, provider2.identifier) + } + + class MockComponent: Component, EmptyInitializable { + var value: String + + required init() { + self.value = "" + } + } + + class MockComponent2: Component, EmptyInitializable { + var value: Bool + + required init() { + self.value = false + } + } + +} + +class ComponentSingletonProviderTests: XCTestCase { + func testProviderReturnsAnInstanceOfType() { + let provider = ComponentSingletonProvider(type: MockComponent.self) + let component: MockComponent? = provider.getComponent() + XCTAssertNotNil(component) + } + + func testProviderReturnsSameInstanceEachTime() { + let provider = ComponentSingletonProvider(type: MockComponent.self) + let component1: MockComponent? = provider.getComponent() + let component2: MockComponent? = provider.getComponent() + XCTAssertTrue(component1 === component2) + + } + + func testProvidersWithSameTypeHaveDifferentIdentifier() { + let provider1 = ComponentSingletonProvider(type: MockComponent.self) + let provider2 = ComponentSingletonProvider(type: MockComponent.self) + XCTAssertNotEqual(provider1.identifier, provider2.identifier) + } + + func testProvidersWithDifferentTypeHaveDifferentIdentifier() { + let provider1 = ComponentSingletonProvider(type: MockComponent.self) + let provider2 = ComponentSingletonProvider(type: MockComponent2.self) + XCTAssertNotEqual(provider1.identifier, provider2.identifier) + } + + class MockComponent: Component, EmptyInitializable { + var value: Int + + required init() { + self.value = 0 + } + } + + class MockComponent2: Component, EmptyInitializable { + var value: String + + required init() { + self.value = "" + } + } +} + +class DynamicComponentProviderTests: XCTestCase { + func testProviderReturnsTheInstance() { + let instance = MockComponent(value: 0) + let providerMethod = DynamicComponentProvider.Closure { instance } + let provider = DynamicComponentProvider(closure: providerMethod) + let component: MockComponent? = provider.getComponent() + XCTAssertTrue(component === instance) + } + + func testProvidersWithSameMethodHaveSameIdentifier() { + let instance = MockComponent(value: 0) + let providerMethod = DynamicComponentProvider.Closure { instance } + let provider1 = DynamicComponentProvider(closure: providerMethod) + let provider2 = DynamicComponentProvider(closure: providerMethod) + XCTAssertEqual(provider1.identifier, provider2.identifier) + } + + func testProvidersWithDifferentMethodsHaveDifferentIdentifier() { + let instance = MockComponent(value: 0) + let providerMethod1 = DynamicComponentProvider.Closure { instance } + let providerMethod2 = DynamicComponentProvider.Closure { instance } + let provider1 = DynamicComponentProvider(closure: providerMethod1) + let provider2 = DynamicComponentProvider(closure: providerMethod2) + XCTAssertNotEqual(provider1.identifier, provider2.identifier) + } + + class MockComponent: Component { + let value: Int + + init(value: Int) { + self.value = value + } + } +}