Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/RolePlayingCore/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ public struct Configuration {
for skillsFile in configurationFiles.skills {
let jsonData = try bundle.loadJSON(skillsFile)
let skills = try jsonDecoder.decode(Skills.self, from: jsonData)
self.skills.skills += skills.skills
self.skills.add(skills.all)
}

for backgroundsFile in configurationFiles.backgrounds {
let jsonData = try bundle.loadJSON(backgroundsFile)
let backgrounds = try jsonDecoder.decode(Backgrounds.self, from: jsonData, configuration: self)
self.backgrounds.backgrounds += backgrounds.backgrounds
self.backgrounds.add(backgrounds.all)
}

for speciesFile in configurationFiles.species {
Expand Down
31 changes: 15 additions & 16 deletions Sources/RolePlayingCore/Currency/Currencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,40 @@
import Foundation

/// A collection of currencies.
public struct Currencies {
public struct Currencies: Codable {

/// A dictionary of all currently loaded currencies.
/// A dictionary of currencies indexed by currency symbol.
private var allCurrencies: [String: UnitCurrency] = [:]

/// Returns a currencies instance that can access a currency by name, and a base unit currency (if one is specified).
init(_ currencies: [UnitCurrency] = []) {
/// A read-only array of currencies.
var all: [UnitCurrency] { Array(allCurrencies.values) }

/// Returns a currencies instance that can access a currency by symbol, and a base unit currency if specified.
public init(_ currencies: [UnitCurrency] = []) {
add(currencies)
}

/// Returns the currency with the specified name.
///
/// - parameter symbol: The shorthand name of the currency.
public func find(_ symbol: String) -> UnitCurrency? { allCurrencies[symbol] }

/// Adds the array of currencies to the collection.
/// Accesses the currency with the specified symbol.
public subscript(symbol: String) -> UnitCurrency? {
return allCurrencies[symbol]
}

/// Adds the array of currencies to the collection. Updates the default or base unit currency if specified.
mutating func add(_ currencies: [UnitCurrency]) {
allCurrencies = Dictionary(currencies.map { ($0.symbol, $0) }, uniquingKeysWith: { _, last in last })

if let baseCurrency = allCurrencies.first(where: { $0.value.isDefault }) {
UnitCurrency.setBaseUnit(baseCurrency.value)
}
}

/// Returns a read-only array of all currencies.
var all: [UnitCurrency] { Array(allCurrencies.values) }
}

extension Currencies: Codable {
// MARK: Codable conformance

private enum CodingKeys: String, CodingKey {
case currencies
}

/// Decodes an array of currencies, setting the default currency if specified.
/// Decodes an array of currencies, updating the default or base unit currency if specified.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

Expand Down
36 changes: 26 additions & 10 deletions Sources/RolePlayingCore/Player/Backgrounds.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,39 @@ import Foundation
/// A collection of backgrounds.
public struct Backgrounds: CodableWithConfiguration {

public var backgrounds: [BackgroundTraits]
/// A dictionary of background traits indexed by name.
private var allBackgrounds: [String: BackgroundTraits] = [:]

/// An array of background traits.
public var all: [BackgroundTraits] { Array(allBackgrounds.values) }

/// Returns an instance of a collection of background traits.
public init(_ backgrounds: [BackgroundTraits] = []) {
self.backgrounds = backgrounds
add(backgrounds)
}

/// Adds the array of background traits to the collection.
mutating func add(_ backgrounds: [BackgroundTraits]) {
allBackgrounds = Dictionary(backgrounds.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last })
}

public func find(_ backgroundName: String) -> BackgroundTraits? {
return backgrounds.first(where: { $0.name == backgroundName })
/// Accesses a background traits instance by name.
public subscript(backgroundName: String) -> BackgroundTraits? {
return allBackgrounds[backgroundName]
}

public var count: Int { backgrounds.count }
/// Returns the number of background traits in the collection.
public var count: Int { allBackgrounds.count }

/// Accesses a background traits instance by index.
public subscript(index: Int) -> BackgroundTraits? {
guard index >= 0 && index < backgrounds.count else { return nil }
return backgrounds[index]
guard index >= 0 && index < count else { return nil }
return all[index]
}

/// Returns a random background traits instance using the specified random index generator.
public func randomElementByIndex<G: RandomIndexGenerator>(using generator: inout G) -> BackgroundTraits {
return backgrounds.randomElementByIndex(using: &generator)!
return all.randomElementByIndex(using: &generator)!
}

// MARK: Codable conformance
Expand All @@ -40,11 +54,13 @@ public struct Backgrounds: CodableWithConfiguration {

public init(from decoder: Decoder, configuration: Configuration) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.backgrounds = try values.decode([BackgroundTraits].self, forKey: .backgrounds, configuration: configuration)

let backgrounds = try values.decode([BackgroundTraits].self, forKey: .backgrounds, configuration: configuration)
add(backgrounds)
}

public func encode(to encoder: Encoder, configuration: Configuration) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(backgrounds, forKey: .backgrounds, configuration: configuration)
try container.encode(all, forKey: .backgrounds, configuration: configuration)
}
}
4 changes: 2 additions & 2 deletions Sources/RolePlayingCore/Player/Player.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public class Player: CodableWithConfiguration {
let skillNames = try values.decode([String].self, forKey: .skillProficiencies)
var resolvedSkills: [Skill] = []
for skillName in skillNames {
guard let skill = configuration.skills.find(skillName) else {
guard let skill = configuration.skills[skillName] else {
throw missingTypeError("skill", skillName)
}
resolvedSkills.append(skill)
Expand All @@ -156,7 +156,7 @@ public class Player: CodableWithConfiguration {
let money = try values.decode(Money.self, forKey: .money, configuration: configuration.currencies)

// Resolve backgroundTraits from configuration
guard let backgroundTraits = configuration.backgrounds.find(backgroundName) else {
guard let backgroundTraits = configuration.backgrounds[backgroundName] else {
throw missingTypeError("background", backgroundName)
}

Expand Down
52 changes: 41 additions & 11 deletions Sources/RolePlayingCore/Player/Skills.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,59 @@ extension Skill: Hashable { }
/// A collection of skills.
public struct Skills: Codable {

public var skills = [Skill]()
/// A dictionary of skills indexed by name.
private var allSkills: [String: Skill] = [:]

private enum CodingKeys: String, CodingKey {
case skills
/// An array of skills.
public var all: [Skill] { Array(allSkills.values) }

/// Returns a skills instance that can access a skill by name.
public init(_ skills: [Skill] = []) {
add(skills)
}

public func find(_ skillName: String?) -> Skill? {
return skills.first(where: { $0.name == skillName })
/// Adds the array of skills to the collection.
mutating func add(_ skills: [Skill]) {
allSkills = Dictionary(skills.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last })
}

/// Accesses a skill by name.
public subscript(skillName: String) -> Skill? {
return allSkills[skillName]
}

public var count: Int { return skills.count }
/// Returns the number of skills in the collection.
public var count: Int { return allSkills.count }

/// Accesses a skill by index.
public subscript(index: Int) -> Skill? {
get {
return skills[index]
}
return all[index]
}

// MARK: Codable conformance

private enum CodingKeys: String, CodingKey {
case skills
}

public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let skills = try container.decode([Skill].self, forKey: .skills)
add(skills)
}

public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(all, forKey: .skills)
}
}

extension Sequence where Element == String {

/// Returns an array of skills from this array of skill names, using the skills argument.
/// Returns an array of skills from this array of skill names, using the specified skills collection.
public func skills(from skills: Skills) throws -> [Skill] {
try self.map { skillName in
guard let skill = skills.find(skillName) else {
guard let skill = skills[skillName] else {
throw missingTypeError("skill", skillName)
}
return skill
Expand All @@ -66,8 +94,10 @@ extension Sequence where Element == Skill {
return selected
}

/// Returns an array of skill names corresponding to this array of skills.
public var skillNames: [String] { self.map(\.name) }

/// Appends an array of skills to this array of skills.
public mutating func append(_ other: Self) {
let selfSet = Set(self)
let otherSet = Set(other)
Expand Down
17 changes: 7 additions & 10 deletions Tests/RolePlayingCoreTests/BackgroundsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,23 +121,20 @@ struct BackgroundsTests {
#expect(backgrounds.count == 3, "Should have 3 backgrounds")

// Then: The find method should locate backgrounds by name
let acolyte = try #require(backgrounds.find("Acolyte"))
let acolyte = try #require(backgrounds["Acolyte"])
#expect(acolyte.name == "Acolyte", "Found background should be Acolyte")
#expect(acolyte.feat == "Magic Initiate", "Acolyte feat should match")

let criminal = try #require(backgrounds.find("Criminal"))
let criminal = try #require(backgrounds["Criminal"])
#expect(criminal.skillProficiencies.skillNames == ["Deception", "Stealth"], "Criminal skills should match")

// Then: The find method should return nil for non-existent backgrounds
let nonExistent = backgrounds.find("Wizard")
let nonExistent = backgrounds["Wizard"]
#expect(nonExistent == nil, "Should not find non-existent background")

// Then: Subscript access should work correctly
let firstBackground = try #require(backgrounds[0])
#expect(firstBackground.name == "Acolyte", "First background should be Acolyte")

let thirdBackground = try #require(backgrounds[2])
#expect(thirdBackground.name == "Soldier", "Third background should be Soldier")
_ = try #require(backgrounds[0])
_ = try #require(backgrounds[2])

// Then: Round-trip encoding should preserve data
let encoder = JSONEncoder()
Expand All @@ -146,7 +143,7 @@ struct BackgroundsTests {
let decodedBackgrounds = try decoder.decode(Backgrounds.self, from: encodedData, configuration: configuration)

#expect(decodedBackgrounds.count == backgrounds.count, "Count should match after round-trip")
#expect(decodedBackgrounds.find("Criminal")?.name == "Criminal", "Should find Criminal after round-trip")
#expect(decodedBackgrounds[1]?.toolProficiency == backgrounds[1]?.toolProficiency, "Tool proficiency should match after round-trip")
let decodedCriminal = try #require(decodedBackgrounds["Criminal"])
#expect(decodedCriminal.toolProficiency == backgrounds["Criminal"]?.toolProficiency, "Should find Criminal with matching tool proficiency after round-trip")
}
}
24 changes: 12 additions & 12 deletions Tests/RolePlayingCoreTests/CurrencyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,20 @@ struct UnitCurrencyTests {

@Test("Unit currency calculations")
func unitCurrency() async throws {
#expect(UnitCurrency.baseUnit() == currencies.find("gp"), "base unit should be goldPieces")
#expect(UnitCurrency.baseUnit() == currencies["gp"], "base unit should be goldPieces")

let goldPieces = Money(value: 25, unit: currencies.find("gp")!)
let silverPieces = Money(value: 12, unit: currencies.find("sp")!)
let copperPieces = Money(value: 1, unit: currencies.find("cp")!)
let electrumPieces = Money(value: 2, unit: currencies.find("ep")!)
let platinumPieces = Money(value: 2, unit: currencies.find("pp")!)
let goldPieces = Money(value: 25, unit: currencies["gp"]!)
let silverPieces = Money(value: 12, unit: currencies["sp"]!)
let copperPieces = Money(value: 1, unit: currencies["cp"]!)
let electrumPieces = Money(value: 2, unit: currencies["ep"]!)
let platinumPieces = Money(value: 2, unit: currencies["pp"]!)

let totalPieces = goldPieces + silverPieces - copperPieces + electrumPieces - platinumPieces

// Should be 25 + 1.2 - 0.01 + 1 - 20
#expect(abs(totalPieces.value - 7.19) < 0.0001, "adding coins should equal 7.19")

let totalPiecesInCopper = totalPieces.converted(to: currencies.find("cp")!)
let totalPiecesInCopper = totalPieces.converted(to: currencies["cp"]!)
#expect(abs(totalPiecesInCopper.value - 719) < 0.01, "adding coins converted to copper should equal 719")
}

Expand All @@ -72,11 +72,11 @@ struct UnitCurrencyTests {
let gpDefault = formatter.string(from: goldPieces)
#expect(gpDefault == "13.7 gp", "gold pieces")

let silverPieces = goldPieces.converted(to: currencies.find("sp")!)
let silverPieces = goldPieces.converted(to: currencies["sp"]!)
let sp = formatter.string(from: silverPieces)
#expect(sp == "137 sp", "silver pieces")

let platinumPieces = goldPieces.converted(to: currencies.find("pp")!)
let platinumPieces = goldPieces.converted(to: currencies["pp"]!)
let ppProvided = formatter.string(from: platinumPieces)
#expect(ppProvided == "1.37 pp", "platinum pieces")

Expand Down Expand Up @@ -109,8 +109,8 @@ struct UnitCurrencyTests {
let cp = "3.2 cp".parseMoney(currencies)
let unwrappedCp = try #require(cp, "coinage as cp should not be nil")
#expect(abs(unwrappedCp.value - 3.2) < 0.0001, "coinage as string cp should be 3.2")
#expect(unwrappedCp.unit == currencies.find("cp"), "coinage as string cp should be copper pieces")
#expect(unwrappedCp.unit != currencies.find("pp"), "coinage as string cp should not be platinum pieces")
#expect(unwrappedCp.unit == currencies["cp"], "coinage as string cp should be copper pieces")
#expect(unwrappedCp.unit != currencies["pp"], "coinage as string cp should not be platinum pieces")

let invalid = "hello".parseMoney(currencies)
#expect(invalid == nil, "coinage as string with hello should be nil")
Expand Down Expand Up @@ -182,7 +182,7 @@ struct UnitCurrencyTests {
}
}

let moneyContainer = MoneyContainer(money: Money(value: 48.93, unit: currencies.find("sp")!))
let moneyContainer = MoneyContainer(money: Money(value: 48.93, unit: currencies["sp"]!))
let encoder = JSONEncoder()
let encoded = try encoder.encode(moneyContainer)
let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String]
Expand Down