From 5b5175a04c79648a15c7cb6584eccc7729bd83a5 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Mon, 27 Oct 2025 09:04:54 -0400 Subject: [PATCH 01/33] Added BackgroundTraits and Backgrounds. Removed speciesTraits ability modifiers. Added starting skills and proficiencies, starting equipment choices, . --- .../project.pbxproj | 4 + .../Configuration/Backgrounds.json | 132 +++++++++++ .../Configuration/Classes.json | 90 ++++++-- .../Configuration/Configuration.json | 1 + .../Configuration/Species.json | 54 ++--- .../Player/CharacterSheet.swift | 13 +- .../RolePlayingCore.xcodeproj/project.pbxproj | 20 ++ .../Configuration/CharacterGenerator.swift | 3 +- .../Configuration/Configuration.swift | 11 +- .../RolePlayingCore/Player/Ability.swift | 5 +- .../Player/BackgroundTraits.swift | 49 ++++ .../RolePlayingCore/Player/Backgrounds.swift | 34 +++ .../RolePlayingCore/Player/ClassTraits.swift | 71 +++++- .../RolePlayingCore/Player/Initiative.swift | 1 + .../RolePlayingCore/Player/Player.swift | 47 +++- .../RolePlayingCore/Player/Players.swift | 13 +- .../RolePlayingCore/Player/Skill.swift | 73 ++++++ .../Player/SpeciesTraits.swift | 65 +++--- .../BackgroundsTests.swift | 149 ++++++++++++ .../ClassTraitsTests.swift | 217 +++++++++++++++++- .../ConfigurationTests.swift | 11 - .../RolePlayingCoreTests/PlayerTests.swift | 71 ++++-- .../RolePlayingCoreTests/PlayersTests.swift | 14 +- .../SpeciesTraitsTests.swift | 28 +-- .../RolePlayingCoreTests/TestBackgrounds.json | 37 +++ .../TestCharacterGenerator.json | 1 + .../RolePlayingCoreTests/TestClasses.json | 16 +- .../TestConfiguration.json | 1 + .../RolePlayingCoreTests/TestMoreClasses.json | 32 +-- .../RolePlayingCoreTests/TestMoreSpecies.json | 21 +- .../RolePlayingCoreTests/TestPlayers.json | 6 + .../RolePlayingCoreTests/TestSpecies.json | 35 +-- 32 files changed, 1099 insertions(+), 226 deletions(-) create mode 100644 CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json create mode 100644 RolePlayingCore/RolePlayingCore/Player/BackgroundTraits.swift create mode 100644 RolePlayingCore/RolePlayingCore/Player/Backgrounds.swift create mode 100644 RolePlayingCore/RolePlayingCore/Player/Skill.swift create mode 100644 RolePlayingCore/RolePlayingCoreTests/BackgroundsTests.swift create mode 100644 RolePlayingCore/RolePlayingCoreTests/TestBackgrounds.json diff --git a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj index 6795738..6d815bb 100644 --- a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj +++ b/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ B621A3B01F0C1C7400E55236 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B621A3AF1F0C1C7400E55236 /* Assets.xcassets */; }; B621A3B31F0C1C7400E55236 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B621A3B11F0C1C7400E55236 /* LaunchScreen.storyboard */; }; B621A3BE1F0C1C7500E55236 /* CharacterGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */; }; + B626FA5F2EAE829D00359F01 /* Backgrounds.json in Resources */ = {isa = PBXBuildFile; fileRef = B626FA5E2EAE829300359F01 /* Backgrounds.json */; }; B64F68E02EA6867B006D6C77 /* CharacterGeneratorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */; }; B64F68E22EA68686006D6C77 /* PlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68E12EA68686006D6C77 /* PlayerListView.swift */; }; B64F68E42EA6868A006D6C77 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68E32EA6868A006D6C77 /* PlayerDetailView.swift */; }; @@ -65,6 +66,7 @@ B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorTests.swift; sourceTree = ""; }; B621A3BF1F0C1C7500E55236 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B621A3D61F0C1E3500E55236 /* GenericPlayer image license.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "GenericPlayer image license.txt"; sourceTree = ""; }; + B626FA5E2EAE829300359F01 /* Backgrounds.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Backgrounds.json; sourceTree = ""; }; B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorApp.swift; sourceTree = ""; }; B64F68E12EA68686006D6C77 /* PlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListView.swift; sourceTree = ""; }; B64F68E32EA6868A006D6C77 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = ""; }; @@ -175,6 +177,7 @@ children = ( B6A1AE441F0C4383008ADF08 /* Configuration.json */, B6A1AE431F0C4383008ADF08 /* Classes.json */, + B626FA5E2EAE829300359F01 /* Backgrounds.json */, B6A1AE451F0C4383008ADF08 /* Currencies.json */, B6A1AE461F0C4383008ADF08 /* Species.json */, B6A1AE4B1F0C5ABF008ADF08 /* SpeciesNames.json */, @@ -304,6 +307,7 @@ B6A1AE481F0C4421008ADF08 /* Configuration.json in Resources */, B6A1AE4A1F0C4429008ADF08 /* Species.json in Resources */, B621A3B31F0C1C7400E55236 /* LaunchScreen.storyboard in Resources */, + B626FA5F2EAE829D00359F01 /* Backgrounds.json in Resources */, B6A1AE491F0C4426008ADF08 /* Currencies.json in Resources */, B621A3B01F0C1C7400E55236 /* Assets.xcassets in Resources */, B6A1AE471F0C441E008ADF08 /* Classes.json in Resources */, diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json b/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json new file mode 100644 index 0000000..5589999 --- /dev/null +++ b/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json @@ -0,0 +1,132 @@ +{ + "backgrounds": [ + { + "name": "Acolyte", + "ability scores": ["Intelligence", "Wisdom", "Charisma"], + "feat": "Magic Initiate (Cleric)", + "skill proficiencies" : ["Insight", "Religion"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Calligrapher's Supplies", "Book (prayers)", "Holy Symbol", "Parchment (10 sheets)", "Robe", "8 GP"], ["50 GP"]] + }, + { + "name": "Artisan", + "ability scores": ["Strength", "Dexterity", "Intelligence"], + "feat": "Crafter", + "skill proficiencies" : ["Investigation", "Persuasion"], + "tool proficiency": "Artisan's Tools", + "equipment": [["Artisan's Tools", "2 Pouches", "Traveler's Clothes", "32 GP"], ["50 GP"]] + }, + { + "name": "Charlatan", + "ability scores": ["Dexterity", "Constitution", "Charisma"], + "feat": "Skilled", + "skill proficiencies" : ["Deception", "Sleight of Hand"], + "tool proficiency": "Forgery Kit", + "equipment": [["Forgery Kit", "Costume", "Fine Clothes", "15 GP"], ["50 GP"]] + }, + { + "name": "Criminal", + "ability scores": ["Dexterity", "Constitution", "Intelligence"], + "feat": "Alert", + "skill proficiencies" : ["Sleight of Hand", "Stealth"], + "tool proficiency": "Thieves' Tools", + "equipment": [["2 Daggers", "Thieves' Tools", "Crowbar", "2 Pouches", "Traveler's Clothes", "16 GP"], ["50 GP"]] + }, + { + "name": "Entertainer", + "ability scores": ["Strength", "Dexterity", "Charisma"], + "feat": "Musician", + "skill proficiencies" : ["Acrobatics", "Performance"], + "tool proficiency": "Musical Instrument", + "equipment": [["Musical Instrument", "2 Costumes", "Mirror", "Perfume", "Traveler's Clothes", "11 GP"], ["50 GP"]] + }, + { + "name": "Farmer", + "ability scores": ["Strength", "Constitution", "Wisdom"], + "feat": "Tough", + "skill proficiencies" : ["Animal Handling", "Nature"], + "tool proficiency": "Carpenter's Tools", + "equipment": [["Sickle", "Carpenter's Tools", "Healer's Kit", "Iron Pot", "Shovel", "Traveler's Clothes", "30 GP"], ["50 GP"]] + }, + { + "name": "Guard", + "ability scores": ["Strength", "Intelligence", "Wisdom"], + "feat": "Alert", + "skill proficiencies" : ["Athletics", "Perception"], + "tool proficiency": "Gaming Set", + "equipment": [["Spear", "Light Crossbow", "20 Bolts", "Gaming Set", "Hooded Lantern", "Manacles", "Quiver", "Traveler's Clothes", "12 GP"], ["50 GP"]] + }, + { + "name": "Guide", + "ability scores": ["Dexterity", "Constitution", "Wisdom"], + "feat": "Magic Initiate (Druid)", + "skill proficiencies" : ["Stealth", "Survival"], + "tool proficiency": "Cartographer's Tools", + "equipment": [["Shortbow", "20 Arrows", "Cartographer's Tools", "Bedroll", "Quiver", "Tent", "Traveler's Clothes", "3 GP"], ["50 GP"]] + }, + { + "name": "Hermit", + "ability scores": ["Constitution", "Wisdom", "Charisma"], + "feat": "Healer", + "skill proficiencies" : ["Medicine", "Religion"], + "tool proficiency": "Herbalism Kit", + "equipment": [["Quarterstaff", "Herbalism Kit", "Bedroll", "Book (philosophy)", "Lamp", "Oil (3 flasks)", "Traveler's Clothes", "16 GP"], ["50 GP"]] + }, + { + "name": "Merchant", + "ability scores": ["Constitution", "Intelligence", "Charisma"], + "feat": "Lucky", + "skill proficiencies" : ["Animal Handling", "Persuasion"], + "tool proficiency": "Navigator's Tools", + "equipment": [["Navigator's Tools", "2 Pouches", "Traveler's Clothes", "22 GP"], ["50 GP"]] + }, + { + "name": "Noble", + "ability scores": ["Strength", "Intelligence", "Charisma"], + "feat": "Skilled", + "skill proficiencies" : ["History", "Persuasion"], + "tool proficiency": "Gaming Set", + "equipment": [["Gaming Set", "Fine Clothes", "Perfume", "29 GP"], ["50 GP"]] + }, + { + "name": "Sage", + "ability scores": ["Constitution", "Intelligence", "Wisdom"], + "feat": "Magic Initiate (Wizard)", + "skill proficiencies" : ["Arcana", "History"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Quarterstaff", "Calligrapher's Supplies", "Book (history)", "Parchment (8 sheets)", "Robe", "8 GP"], ["50 GP"]] + }, + { + "name": "Sailor", + "ability scores": ["Strength", "Dexterity", "Wisdom"], + "feat": "Tavern Brawler", + "skill proficiencies" : ["Acrobatics", "Perception"], + "tool proficiency": "Navigator's Tools", + "equipment": [["Dagger", "Navigator's Tools", "Rope", "Traveler's Clothes", "20 GP"], ["50 GP"]] + }, + { + "name": "Scribe", + "ability scores": ["Dexterity", "Intelligence", "Wisdom"], + "feat": "Skilled", + "skill proficiencies" : ["Investigation", "Perception"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Calligrapher's Supplies", "Fine Clothes", "Lamp", "Oil (3 flasks)", "Parchment (12 sheets)", "23 GP"], ["50 GP"]] + }, + { + "name": "Soldier", + "ability scores": ["Strength", "Dexterity", "Constitution"], + "feat": "Savage Attacker", + "skill proficiencies" : ["Athletics", "Intimidation"], + "tool proficiency": "Gaming Set", + "equipment": [["Spear", "Shortbow", "20 Arrows", "Gaming Set", "Healer's Kit", "Quiver", "Traveler's Clothes", "14 GP"], ["50 GP"]] + }, + { + "name": "Wayfarer", + "ability scores": ["Dexterity", "Wisdom", "Charisma"], + "feat": "Lucky", + "skill proficiencies" : ["Insight", "Stealth"], + "tool proficiency": "Thieves' Tools", + "equipment": [["2 Daggers", "Thieves' Tools", "Gaming Set", "Bedroll", "2 Pouches", "Traveler's Clothes", "16 GP"], ["50 GP"]] + } + ] +} diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Classes.json b/CharacterGenerator/CharacterGenerator/Configuration/Classes.json index 7e8262c..46a665e 100644 --- a/CharacterGenerator/CharacterGenerator/Configuration/Classes.json +++ b/CharacterGenerator/CharacterGenerator/Configuration/Classes.json @@ -27,9 +27,12 @@ "hit dice": "d8", "primary ability": ["Wisdom"], "saving throws": ["Wisdom", "Charisma"], + "starting skill count": 2, + "skill proficiencies": ["History", "Insight", "Medicine", "Persuasion", "Religion"], "starting wealth": "5d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple"], + "starting equipment": [["Chain Shirt", "Shield", "Mace", "Holy Symbol", "Priest's Pack", "7 GP"], ["110 GP"]], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple"], }, { "name": "Fighter", @@ -38,9 +41,12 @@ "primary ability": ["Strength"], "alternate primary ability": ["Dexterity"], "saving throws": ["Strength", "Constitution"], + "starting skill count": 2, + "skill proficiencies": ["Acrobatics", "Animal Handling", "Athletics", "History", "Insight", "Intimidation", "Persuasion", "Perception", "Survival"], "starting wealth": "5d4x10", - "armor": ["all"], - "weapons": ["simple", "martial"], + "starting equipment": [["Chain Mail", "Greatsword", "Flail", "8 Javelins", "Dungeoneer's Pack", "4 GP"], ["Studded Leather Armor", "Scimitar", "Shortsword", "Longbow", "20 Arrows", "Quiver", "Dungeoneer's Pack", "11 GP"], ["155 GP"]], + "armor training": ["light", "medium", "heavy", "shield"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Rogue", @@ -49,9 +55,13 @@ "hit dice": "d8", "primary ability": ["Dexterity"], "saving throws": ["Dexterity", "Intelligence"], + "starting skill count": 4, + "skill proficiencies": ["Acrobatics", "Athletics", "Deception", "Insight", "Intimidation", "Investigation", "Perception", "Persuasion", "Sleight of Hand", "Stealth"], + "tool proficiencies": ["Thieves' Tools"], "starting wealth": "4d4x10", - "armor": ["light"], - "weapons": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + "starting equipment": [["Leather Armor", "2 Daggers", "Shortsword", "Shortbow", "20 Arrows", "Quiver", "Thieves' Tools", "Burglar's Pack", "8 GP"], ["100 GP"]], + "armor training": ["light"], + "weapon proficiencies": ["simple", "martial (finesse or light)"], }, { "name": "Wizard", @@ -59,9 +69,12 @@ "hit dice": "d6", "primary ability": ["Intelligence"], "saving throws": ["Intelligence", "Wisdom"], + "starting skill count": 2, + "skill proficiencies": ["Arcana", "History", "Insight", "Investigation", "Medicine", "Nature", "Religion"], "starting wealth": "4d4x10", - "armor": [], - "weapons": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], + "starting equipment": [["2 Daggers", "Arcane Focus (quarterstaff)", "Robe", "Spellbook", "Scholar's Pack", "5 GP"], ["55 GP"]], + "armor training": [], + "weapon proficiencies": ["simple"], }, { "name": "Barbarian", @@ -69,9 +82,12 @@ "hit dice": "d12", "primary ability": ["Strength"], "saving throws": ["Strength", "Constitution"], + "starting skill count": 2, + "skill proficiencies": ["Animal Handling", "Athletics", "Intimidation", "Nature", "Perception", "Survival"], "starting wealth": "2d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple", "martial"], + "starting equipment": [["Greataxe", "4 Handaxes", "Explorer's Pack", "15 GP"], ["75 GP"]], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Bard", @@ -79,9 +95,13 @@ "hit dice": "d8", "primary ability": ["Charisma"], "saving throws": ["Dexterity", "Charisma"], + "starting skill count": 3, + "tools": 3, + "tool proficiencies": ["Musical Instrument"], "starting wealth": "5d4x10", - "armor": ["light"], - "weapons": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + "starting equipment": [["Leather Armor", "2 Daggers", "Any Tool", "Entertainer's Pack", "19 GP"], ["90 GP"]], + "armor training": ["light"], + "weapon proficiencies": ["simple"], }, { "name": "Druid", @@ -89,9 +109,14 @@ "hit dice": "d8", "primary ability": ["Wisdom"], "saving throws": ["Intelligence", "Wisdom"], + "starting skill count": 2, + "skill proficiencies": ["Arcana", "Animal Handling", "Insight", "Medicine", "Nature", "Perception", "Religion", "Survival"], + "tools": 1, + "tool proficiencies": ["Herbalism Kit"], "starting wealth": "2d4x10", - "armor": ["light", "medium", "shields", "nonmetal"], - "weapons": ["club", "dagger", "dart", "javelin", "mace", "quarterstaff", "scimitar", "sickle", "sling", "spear"], + "starting equipment": [["Leather Armor", "Shield", "Sickle", "Druidic Focus (Quarterstaff)", "Explorer's Pack", "Herbalism Kit", "9 GP"], ["50 GP"]], + "armor training": ["light", "shields"], + "weapon proficiencies": ["simple"], }, { "name": "Monk", @@ -99,9 +124,14 @@ "hit dice": "d8", "primary ability": ["Dexterity", "Wisdom"], "saving throws": ["Strength", "Dexterity"], + "starting skill count": 2, + "skill proficiencies": ["Acrobatics", "Athletics", "History", "Insight", "Religion", "Stealth"], + "tools": 1, + "tool proficiencies": ["Artisan's Tools", "Musical Instrument"], "starting wealth": "5d4", - "armor": [], - "weapons": ["simple", "shortsword"], + "starting equipment": [["Spear", "5 Daggers", "Any Tool", "Explorer's Pack", "11 GP"], ["50 GP"]], + "armor training": [], + "weapon proficiencies": ["simple", "martial (light)"], }, { "name": "Paladin", @@ -109,9 +139,12 @@ "hit dice": "d10", "primary ability": ["Strength", "Charisma"], "saving throws": ["Wisdom", "Charisma"], + "starting skill count": 2, + "skill proficiencies": ["Athletics", "Insight", "Intimidation", "Medicine", "Persuasion", "Religion"], "starting wealth": "5d4x10", - "armor": ["all"], - "weapons": ["simple", "martial"], + "starting equipment": [["Chain Mail", "Shield", "Longsword", "6 Javelins", "Holy Symbol", "Priest's Pack", "9 GP"], ["150 GP"]], + "armor training": ["light", "medium", "heavy", "shield"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Ranger", @@ -119,9 +152,12 @@ "hit dice": "d10", "primary ability": ["Dexterity", "Wisdom"], "saving throws": ["Strength", "Dexterity"], + "starting skill count": 3, + "skill proficiencies": ["Animal Handling", "Athletics", "Insight", "Investigation", "Nature", "Perception", "Stealth", "Survival"], "starting wealth": "5d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple", "martial"], + "starting equipment": [["Studded Leather Armor", "Scimitar", "Shortsword", "Longbow", "20 Arrows", "Quiver", "Druidic Focus (sprig of mistletoe)", "Explorer's Pack", "7 GP"], ["150 GP"]], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Sorcerer", @@ -129,9 +165,12 @@ "hit dice": "d6", "primary ability": ["Charisma"], "saving throws": ["Constitution", "Charisma"], + "starting skill count": 2, + "skill proficiencies": ["Arcana", "Deception", "Insight", "Intimidation", "Persuasion", "Religion"], "starting wealth": "3d4x10", - "armor": [], - "weapons": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], + "starting equipment": [["Spear", "2 Daggers", "Arcane Focus (crystal)", "Dungeoneer's Pack", "28 GP"], ["50 GP"]], + "armor training": [], + "weapon proficiencies": ["simple"], }, { "name": "Warlock", @@ -139,9 +178,12 @@ "hit dice": "d8", "primary ability": ["Charisma"], "saving throws": ["Wisdom", "Charisma"], + "starting skill count": 2, + "skill proficiencies": ["Arcana", "Deception", "History", "Intimidation", "Investigation", "Nature", "Religion"], "starting wealth": "4d4x10", - "armor": ["light"], - "weapons": ["simple"], + "starting equipment": [["Leather Armor", "Sickle", "2 Daggers", "Arcane Focus (orb)", "Book (occult lore)", "Scholar's Pack", "15 GP"], ["100 GP"]], + "armor training": ["light"], + "weapon proficiencies": ["simple"], } ] } diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json b/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json index 68336eb..bffe9a7 100644 --- a/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json +++ b/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json @@ -1,5 +1,6 @@ { "currencies": ["Currencies"], + "backgrounds": ["Backgrounds"], "classes": ["Classes"], "species": ["Species"], "species names": "SpeciesNames" diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Species.json b/CharacterGenerator/CharacterGenerator/Configuration/Species.json index d8875a9..0f29c13 100644 --- a/CharacterGenerator/CharacterGenerator/Configuration/Species.json +++ b/CharacterGenerator/CharacterGenerator/Configuration/Species.json @@ -3,7 +3,6 @@ { "name": "Dwarf", "plural": "Dwarves", - "ability scores": { "Constitution": 2 }, "minimum age": 50, "lifespan": 350, "alignment": "Lawful Good", @@ -13,15 +12,14 @@ "weight modifier": "2d6", "speed": 25, "darkvision": 60, - "skills": ["dwarven resilience"], - "weapons" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], + "skill proficiencies": ["dwarven resilience"], + "weapon proficiencies" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], "tools": ["smith's tools", "brewer's supplies", "mason's tools"], "history": ["stonecunning"], "languages": ["Common", "Dwarvish"], "subspecies": [{ "name": "Hill Dwarf", "plural": "Hill Dwarves", - "ability scores": { "Wisdom": 2 }, "hit point bonus": 1, "base height": "3'5\"", "base weight": 115, @@ -29,14 +27,12 @@ { "name": "Mountain Dwarf", "plural": "Mountain Dwarves", - "ability scores": { "Strength": 2 }, "armor": ["light", "medium"], }] }, { "name": "Elf", "plural": "Elves", - "ability scores": { "Dexterity": 2 }, "minimum age": 100, "lifespan": 750, "alignment": "Chaotic Good", @@ -47,14 +43,13 @@ "speed": 30, "darkvision": 60, "resilience": ["poison", "poison damage"], - "skills": ["keen senses", "fey ancestry", "trance"], + "skill proficiencies": ["keen senses", "fey ancestry", "trance"], "languages": ["Common", "Elvish"], "subspecies": [{ "name": "High Elf", "plural": "High Elves", - "ability scores": { "Intelligence": 1 }, "base weight:": 90, - "weapons": ["longsword", "shortsword", "shortbow", "longbow"], + "weapon proficiencies": ["longsword", "shortsword", "shortbow", "longbow"], "extra languages": 1, "spells": ["wizard cantrip"], "hit point bonus": 1 @@ -62,28 +57,25 @@ { "name": "Wood Elf", "plural": "Wood Elves", - "ability scores": { "Wisdom": 1 }, - "weapons": ["longsword", "shortsword", "shortbow", "longbow"], - "skills": ["fleet of foot", "mask of the wild"] + "weapon proficiencies": ["longsword", "shortsword", "shortbow", "longbow"], + "skill proficiencies": ["fleet of foot", "mask of the wild"] }, { "name": "Dark Elf", "plural": "Dark Elves", "aliases": ["Drow"], - "ability scores": { "Charisma": 1 }, "base height": "4'5\"", "height modifier": "2d6", "base weight": 75, "weight modifier": "1d6", "darkvision": 120, - "weapons": ["rapier", "shortsword", "hand crossbow"], + "weapon proficiencies": ["rapier", "shortsword", "hand crossbow"], "spells": ["dancing lights", "faerie fire:3", "darkness:5"] }] }, { "name": "Halfling", "plural": "Halflings", - "ability scores": { "Dexterity": 2 }, "minimum age": 20, "lifespan": 150, "alignment": "Lawful Good", @@ -91,28 +83,25 @@ "base weight": 35, "height modifier": "2d4", "speed": 25, - "skills": ["lucky", "brave", "halfling nimbleness"], - "weapons" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], + "skill proficiencies": ["lucky", "brave", "halfling nimbleness"], + "weapon proficiencies" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], "tools": ["smith's tools", "brewer's supplies", "mason's tools"], "history": ["stonecunning"], "languages": ["Common", "Halfling"], "subspecies": [{ "name": "Lightfoot", "plural": "Lightfoots", - "ability scores": { "Charisma": 1 }, - "skills": ["stealthy"] + "skill proficiencies": ["stealthy"] }, { "name": "Stout", "plural": "Stouts", - "ability scores": { "Constitution": 1 }, - "skills": ["stout resilience"] + "skill proficiencies": ["stout resilience"] }] }, { "name": "Human", "plural": "Humans", - "ability scores": { "Strength": 1, "Dexterity": 1, "Constitution": 1, "Intelligence": 1, "Wisdom": 1, "Charisma": 1 }, "minimum age": 18, "lifespan": 90, "base height": "4'8\"", @@ -126,7 +115,6 @@ { "name": "Dragonborn", "plural": "Dragonborn", - "ability scores": { "Strength": 2, "Charisma": 1 }, "minimum age": 15, "lifespan": 80, "alignment": "Good", @@ -135,13 +123,12 @@ "base weight": 175, "weight modifier": "2d6", "speed": 30, - "skills": ["draconic ancestry", "breath weapon", "damage"], + "skill proficiencies": ["draconic ancestry", "breath weapon", "damage"], "languages": ["Common", "Draconic"] }, { "name": "Gnome", "plural": "Gnomes", - "ability scores": { "Intelligence": 2 }, "minimum age": 40, "lifespan": 425, "alignment": "Good", @@ -150,25 +137,22 @@ "base weight": 35, "speed": 25, "darkvision": 60, - "skills": ["gnome cunning"], + "skill proficiencies": ["gnome cunning"], "languages": ["Common", "Gnomish"], "subspecies": [{ "name": "Forest Gnome", "plural": "Forest Gnomes", - "ability scores": { "Dexterity": 1 }, - "skills": ["natural illusionist", "speak with small beasts"] + "skill proficiencies": ["natural illusionist", "speak with small beasts"] }, { "name": "Rock Gnome", "plural": "Rock Gnomes", - "ability scores": { "Constitution": 1 }, - "skills": ["artificers lore", "tinker"] + "skill proficiencies": ["artificers lore", "tinker"] }] }, { "name": "Half-Elf", "plural": "Half-Elves", - "ability scores": { "Strength": 1 }, "ability score bonuses": 2, "minimum age": 20, "lifespan": 180, @@ -179,7 +163,7 @@ "weight modifier": "2d4", "speed": 30, "darkvision": 60, - "skills": ["fey ancestry"], + "skill proficiencies": ["fey ancestry"], "languages": ["Common", "Elvish"], "extra languages": 1, "skill versatility": 2 @@ -187,7 +171,6 @@ { "name": "Half-Orc", "plural": "Half-Orcs", - "ability scores": { "Strength": 2, "Constitution": 1 }, "minimum age": 14, "lifespan": 75, "alignment": "Chaotic", @@ -197,13 +180,12 @@ "weight modifier": "2d6", "speed": 30, "darkvision": 60, - "skills": ["menacing", "relentless endurance", "savage attacks"], + "skill proficiencies": ["menacing", "relentless endurance", "savage attacks"], "languages": ["Common", "Orcish"] }, { "name": "Tiefling", "plural": "Tieflings", - "ability scores": { "Intelligence": 1, "Charisma": 2 }, "minimum age": 18, "lifespan": 100, "alignment": "Chaotic", @@ -213,7 +195,7 @@ "weight modifier": "2d4", "speed": 30, "darkvision": 60, - "skills": ["hellish resistance", "infernal legacy"], + "skill proficiencies": ["hellish resistance", "infernal legacy"], "languages": ["Common", "Infernal"] } ] diff --git a/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift b/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift index 9f14332..3ad55ec 100644 --- a/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift +++ b/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift @@ -26,8 +26,9 @@ class CharacterSheet { // Mapping between sections/items and key paths to properties. var keys: [[PartialKeyPath]] = [ [\.experiencePoints], - [\.speciesName, \.className], + [\.backgroundName, \.speciesName, \.className], [\.abilities], + [\.skills], [\.initiative, \.speed, \.size], [\.armorClass, \.proficiencyBonus, \.passivePerception], [\.maximumHitPoints, \.hitDice], @@ -38,8 +39,9 @@ class CharacterSheet { // Mapping of properties to label keys. var labelKeys: [[String]] = [ ["Experience Points"], - ["Species", "Class", "Subclass"], + ["Background", "Species", "Class", "Subclass"], ["Abilities"], + ["Skills"], ["Initiative", "Speed", "Size"], ["Armor Class", "Proficiency Bonus", "Passive Perception"], ["Hit Points", "Hit Dice"], @@ -50,8 +52,9 @@ class CharacterSheet { // Mapping of properties to view types. var cellIdentifiers: [[String]] = [ ["experiencePoints"], - ["labeledText", "labeledText"], + ["labeledText", "labeledText", "labeledText"], ["abilities"], + ["labeledText"], ["labeledNumber", "labeledNumber", "labeledText"], ["labeledNumber", "labeledNumber", "labeledNumber"], ["labeledNumber", "labeledText"], @@ -69,6 +72,7 @@ class CharacterSheet { var experiencePoints: String { "\(player.experiencePoints)" } var level: String { "\(player.level)" } + var backgroundName: String { player.backgroundName } var className: String { player.className } var speciesName: String { player.speciesName } var alignment: String { @@ -79,6 +83,9 @@ class CharacterSheet { } } var abilities: AbilityScores { player.abilities } + var skills: String { + player.skills.map(\.name).joined(separator: ", ") + } var initiative: String { player.initiativeModifier.displayModifier } var armorClass: String { "\(player.armorClass)" } var proficiencyBonus: String { player.proficiencyBonus.displayModifier } diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj index c49d70b..3b3cb29 100644 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj +++ b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj @@ -15,6 +15,11 @@ B620560F1E19DDD0002494AB /* DiceParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B620560C1E19DDD0002494AB /* DiceParser.swift */; }; B621A3981F0C052D00E55236 /* NameGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3971F0C052D00E55236 /* NameGeneratorTests.swift */; }; B621A3991F0C06A700E55236 /* NameGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3961F0C020000E55236 /* NameGenerator.swift */; }; + B626FA592EAE76AE00359F01 /* Skill.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626FA582EAE76AA00359F01 /* Skill.swift */; }; + B626FA5B2EAE803000359F01 /* BackgroundTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626FA5A2EAE802D00359F01 /* BackgroundTraits.swift */; }; + B626FA5D2EAE81C900359F01 /* Backgrounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626FA5C2EAE81C600359F01 /* Backgrounds.swift */; }; + B626FA612EAE919900359F01 /* TestBackgrounds.json in Resources */ = {isa = PBXBuildFile; fileRef = B626FA602EAE919200359F01 /* TestBackgrounds.json */; }; + B626FA642EAF9FCF00359F01 /* BackgroundsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626FA622EAF9F9200359F01 /* BackgroundsTests.swift */; }; B62D89C41F09A3870095D587 /* DiceParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62D89C31F09A3870095D587 /* DiceParserTests.swift */; }; B64C369621756BC300C4F6BE /* DiceRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C369521756BC300C4F6BE /* DiceRoll.swift */; }; B6688B512EACF5B7000A83DD /* Initiative.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6688B502EACF5AE000A83DD /* Initiative.swift */; }; @@ -100,6 +105,11 @@ B620560C1E19DDD0002494AB /* DiceParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceParser.swift; sourceTree = ""; }; B621A3961F0C020000E55236 /* NameGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGenerator.swift; sourceTree = ""; }; B621A3971F0C052D00E55236 /* NameGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGeneratorTests.swift; sourceTree = ""; }; + B626FA582EAE76AA00359F01 /* Skill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Skill.swift; sourceTree = ""; }; + B626FA5A2EAE802D00359F01 /* BackgroundTraits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTraits.swift; sourceTree = ""; }; + B626FA5C2EAE81C600359F01 /* Backgrounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backgrounds.swift; sourceTree = ""; }; + B626FA602EAE919200359F01 /* TestBackgrounds.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TestBackgrounds.json; sourceTree = ""; }; + B626FA622EAF9F9200359F01 /* BackgroundsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundsTests.swift; sourceTree = ""; }; B62D89C31F09A3870095D587 /* DiceParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiceParserTests.swift; sourceTree = ""; }; B64C369521756BC300C4F6BE /* DiceRoll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceRoll.swift; sourceTree = ""; }; B6688B502EACF5AE000A83DD /* Initiative.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initiative.swift; sourceTree = ""; }; @@ -220,6 +230,7 @@ B62055FC1E19DD23002494AB /* RolePlayingCoreTests.swift */, B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */, B6F070351E4F991D00F66918 /* AlignmentTests.swift */, + B626FA622EAF9F9200359F01 /* BackgroundsTests.swift */, B69F846A1E58D33900A4D2B0 /* PlayerTests.swift */, B69F846E1E59155A00A4D2B0 /* PlayersTests.swift */, B6FA6CB51E47B080004D91B1 /* CurrencyTests.swift */, @@ -245,6 +256,7 @@ B69F84721E591DE400A4D2B0 /* InvalidClassPlayers.json */, B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */, B66AF8F41EAE87A800C15F8E /* TestClasses.json */, + B626FA602EAE919200359F01 /* TestBackgrounds.json */, B66AF8F71EAE87A800C15F8E /* TestMoreClasses.json */, B66AF8F51EAE87A800C15F8E /* TestConfiguration.json */, B6F4AA4B1F12CE17000C72D2 /* TestCharacterGenerator.json */, @@ -313,6 +325,9 @@ children = ( B6FA6CC41E4A96D1004D91B1 /* Ability.swift */, B6FA6CCA1E4E7AEA004D91B1 /* Alignment.swift */, + B626FA582EAE76AA00359F01 /* Skill.swift */, + B626FA5A2EAE802D00359F01 /* BackgroundTraits.swift */, + B626FA5C2EAE81C600359F01 /* Backgrounds.swift */, B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */, B6CF53CF1E54E2E200CADD9F /* Classes.swift */, B69F84681E58B8F700A4D2B0 /* Player.swift */, @@ -438,6 +453,7 @@ B69F84751E591E9800A4D2B0 /* InvalidSpeciesPlayers.json in Resources */, B66AF9011EAE87A800C15F8E /* TestNames.json in Resources */, B66AF8FD1EAE87A800C15F8E /* TestConfiguration.json in Resources */, + B626FA612EAE919900359F01 /* TestBackgrounds.json in Resources */, B66AF9091EAE8B8100C15F8E /* MissingClassPlayers.json in Resources */, B69F84731E591DE400A4D2B0 /* InvalidClassPlayers.json in Resources */, B66AF8FF1EAE87A800C15F8E /* TestMoreClasses.json in Resources */, @@ -484,8 +500,11 @@ B681B71A1EAAC8FB001DE78B /* CompoundDice.swift in Sources */, B6FA6CBD1E47B803004D91B1 /* Weight.swift in Sources */, B6FA6CC51E4A96D1004D91B1 /* Ability.swift in Sources */, + B626FA592EAE76AE00359F01 /* Skill.swift in Sources */, B681B7181EAAC8EE001DE78B /* DiceModifier.swift in Sources */, + B626FA5B2EAE803000359F01 /* BackgroundTraits.swift in Sources */, B69F84691E58B8F700A4D2B0 /* Player.swift in Sources */, + B626FA5D2EAE81C900359F01 /* Backgrounds.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -499,6 +518,7 @@ B6F4AA4A1F12CD2A000C72D2 /* CharacterGeneratorTests.swift in Sources */, B69F846F1E59155A00A4D2B0 /* PlayersTests.swift in Sources */, B62D89C41F09A3870095D587 /* DiceParserTests.swift in Sources */, + B626FA642EAF9FCF00359F01 /* BackgroundsTests.swift in Sources */, B6CF53DB1E56443A00CADD9F /* ClassesTests.swift in Sources */, B621A3981F0C052D00E55236 /* NameGeneratorTests.swift in Sources */, B6CF53921E51DEDD00CADD9F /* ClassTraitsTests.swift in Sources */, diff --git a/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift b/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift index 82b45ee..81eba2f 100644 --- a/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift +++ b/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift @@ -41,12 +41,13 @@ public struct CharacterGenerator { let randomClass = generator.randomIndex(upperBound: configuration.classes.count) let gender = Player.Gender.allCases.randomElementByIndex(using: &generator) + let backgroundTraits = configuration.backgrounds.randomElementByIndex(using: &generator) let speciesTraits = configuration.species.randomElementByIndex(using: &generator) let classTraits = configuration.classes[randomClass]! let name = names.randomName(speciesTraits: speciesTraits, gender: gender, using: &generator) let alignment = speciesTraits.alignment != nil ? speciesTraits.alignment : randomAlignment(using: &generator) - return Player(name, speciesTraits: speciesTraits, classTraits: classTraits, gender: gender, alignment: alignment) + return Player(name, backgroundTraits: backgroundTraits, speciesTraits: speciesTraits, classTraits: classTraits, gender: gender, alignment: alignment) } public func makeCharacter() -> Player { diff --git a/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift b/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift index d5a9b6c..a33687b 100644 --- a/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift +++ b/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift @@ -14,6 +14,7 @@ import Foundation public struct ConfigurationFiles: Decodable { let currencies: [String] + let backgrounds: [String] let species: [String] let classes: [String] let players: [String]? @@ -21,6 +22,7 @@ public struct ConfigurationFiles: Decodable { private enum CodingKeys: String, CodingKey { case currencies + case backgrounds case species case classes case players @@ -35,6 +37,7 @@ public struct Configuration { public var configurationFiles: ConfigurationFiles + public var backgrounds = Backgrounds() public var species = Species() public var classes = Classes() public var players = Players() @@ -55,6 +58,12 @@ public struct Configuration { _ = try jsonDecoder.decode(Currencies.self, from: jsonData) } + for backgroundsFile in configurationFiles.backgrounds { + let jsonData = try bundle.loadJSON(backgroundsFile) + let backgrounds = try jsonDecoder.decode(Backgrounds.self, from: jsonData) + self.backgrounds.backgrounds += backgrounds.backgrounds + } + for speciesFile in configurationFiles.species { let jsonData = try bundle.loadJSON(speciesFile) let species = try jsonDecoder.decode(Species.self, from: jsonData) @@ -80,7 +89,7 @@ public struct Configuration { for playersFile in playersFiles { let jsonData = try bundle.loadJSON(playersFile) let players = try jsonDecoder.decode(Players.self, from: jsonData) - try players.resolve(classes: self.classes, species: self.species) + try players.resolve(backgrounds: self.backgrounds, classes: self.classes, species: self.species) self.players.players += players.players } } diff --git a/RolePlayingCore/RolePlayingCore/Player/Ability.swift b/RolePlayingCore/RolePlayingCore/Player/Ability.swift index acb01cd..e1a829e 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Ability.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Ability.swift @@ -232,10 +232,7 @@ extension AbilityScores { /// Creates a set of default ability scores with values initialized to 0. public init(defaults: [Ability] = Ability.defaults) { - scores = [Ability: Int](minimumCapacity: defaults.count) - for ability in defaults { - scores[ability] = 0 - } + scores = Dictionary(uniqueKeysWithValues: defaults.map { ($0, 0) }) } } diff --git a/RolePlayingCore/RolePlayingCore/Player/BackgroundTraits.swift b/RolePlayingCore/RolePlayingCore/Player/BackgroundTraits.swift new file mode 100644 index 0000000..a1dcd5e --- /dev/null +++ b/RolePlayingCore/RolePlayingCore/Player/BackgroundTraits.swift @@ -0,0 +1,49 @@ +// +// BackgroundTraits.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/26/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +public struct BackgroundTraits { + public let name: String + public let abilityScores: [String] + public let feat: String + public let skillProficiencies: [Skill] + public let toolProficiency: String + public let equipment: [[String]] +} + +extension BackgroundTraits: Codable { + private enum CodingKeys: String, CodingKey { + case name + case abilityScores = "ability scores" + case feat + case skillProficiencies = "skill proficiencies" + case toolProficiency = "tool proficiency" + case equipment = "equipment" + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.name = try values.decode(String.self, forKey: .name) + self.abilityScores = try values.decode([String].self, forKey: .abilityScores) + self.feat = try values.decode(String.self, forKey: .feat) + + let skillNames = try values.decode([String].self, forKey: .skillProficiencies) + self.skillProficiencies = Skill.skills(from: skillNames) + self.toolProficiency = try values.decode(String.self, forKey: .toolProficiency) + self.equipment = try values.decode([[String]].self, forKey: .equipment) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(abilityScores, forKey: .abilityScores) + try container.encode(feat, forKey: .feat) + try container.encode(skillProficiencies.skillNames, forKey: .skillProficiencies) + try container.encode(toolProficiency, forKey: .toolProficiency) + try container.encode(equipment, forKey: .equipment) + } +} diff --git a/RolePlayingCore/RolePlayingCore/Player/Backgrounds.swift b/RolePlayingCore/RolePlayingCore/Player/Backgrounds.swift new file mode 100644 index 0000000..9b6f4d9 --- /dev/null +++ b/RolePlayingCore/RolePlayingCore/Player/Backgrounds.swift @@ -0,0 +1,34 @@ +// +// Backgrounds.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/26/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +// A set of background traits +public struct Backgrounds: Codable { + + public var backgrounds = [BackgroundTraits]() + + private enum CodingKeys: String, CodingKey { + case backgrounds + } + + public func find(_ backgroundName: String?) -> BackgroundTraits? { + return backgrounds.first(where: { $0.name == backgroundName }) + } + + public var count: Int { return backgrounds.count } + + public subscript(index: Int) -> BackgroundTraits? { + get { + return backgrounds[index] + } + } + + public func randomElementByIndex(using generator: inout G) -> BackgroundTraits { + return backgrounds.randomElementByIndex(using: &generator)! + } + +} diff --git a/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift b/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift index e3c423a..a5f3a77 100644 --- a/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift +++ b/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift @@ -17,8 +17,15 @@ public struct ClassTraits { public var descriptiveTraits: [String: String] public var primaryAbility: [Ability] + public var alternatePrimaryAbility: [Ability]? public var savingThrows: [Ability] public var experiencePoints: [Int]? + public var startingSkillCount: Int + public var skillProficiencies: [String] + public var weaponProficiencies: [String] + public var toolProficiencies: [String] + public var armorTraining: [String] + public var startingEquipment: [[String]] /// Accesses the experiencePoints array for the specified 1-based level. public func minExperiencePoints(at level: Int) -> Int { @@ -37,13 +44,29 @@ public struct ClassTraits { /// Accesses the maximum experience points for the specified 1-based level. public func maxExperiencePoints(at level: Int) -> Int { + guard level > 0 else { return 0 } + // One less than the minimum for the next level - minExperiencePoints(at: level + 1) - 1 + return minExperiencePoints(at: level + 1) - 1 } // TODO: weapons, armor, skills, etc. - public init(name: String, plural: String, hitDice: Dice, startingWealth: Dice, descriptiveTraits: [String: String] = [:], primaryAbility: [Ability] = [], savingThrows: [Ability] = [], experiencePoints: [Int]? = nil) { + public init(name: String, + plural: String, + hitDice: Dice, + startingWealth: Dice, + descriptiveTraits: [String: String] = [:], + primaryAbility: [Ability] = [], + alternatePrimaryAbility: [Ability]? = nil, + savingThrows: [Ability] = [], + startingSkillCount: Int = 2, + skillProficiencies: [String] = [], + weaponProficiencies: [String] = [], + toolProficiencies: [String] = [], + armorTraining: [String] = [], + startingEquipment: [[String]] = [], + experiencePoints: [Int]? = nil) { self.name = name self.plural = plural self.hitDice = hitDice @@ -51,7 +74,14 @@ public struct ClassTraits { self.descriptiveTraits = descriptiveTraits self.primaryAbility = primaryAbility + self.alternatePrimaryAbility = alternatePrimaryAbility self.savingThrows = savingThrows + self.startingSkillCount = startingSkillCount + self.skillProficiencies = skillProficiencies + self.weaponProficiencies = weaponProficiencies + self.toolProficiencies = toolProficiencies + self.armorTraining = armorTraining + self.startingEquipment = startingEquipment self.experiencePoints = experiencePoints } } @@ -65,7 +95,14 @@ extension ClassTraits: Codable { case startingWealth = "starting wealth" case descriptiveTraits = "descriptive traits" case primaryAbility = "primary ability" + case alternatePrimaryAbility = "alternate primary ability" case savingThrows = "saving throws" + case startingSkillCount = "starting skill count" + case skillProficiencies = "skill proficiencies" + case weaponProficiencies = "weapon proficiencies" + case toolProficiencies = "tool proficiencies" + case armorTraining = "armor training" + case startingEquipment = "starting equipment" case experiencePoints = "experience points" } @@ -80,7 +117,15 @@ extension ClassTraits: Codable { let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) let primaryAbility = try values.decodeIfPresent([Ability].self, forKey: .primaryAbility) + let alternatePrimaryAbility = try values.decodeIfPresent([Ability].self, forKey: .alternatePrimaryAbility) let savingThrows = try values.decodeIfPresent([Ability].self, forKey: .savingThrows) + let startingSkillCount: Int? = try values.decodeIfPresent(Int.self, forKey: .startingSkillCount) + let skillProficiencies = try values.decodeIfPresent([String].self, forKey: .skillProficiencies) + let weaponProficiencies = try values.decodeIfPresent([String].self, forKey: .weaponProficiencies) + let toolProficiencies = try values.decodeIfPresent([String].self, forKey: .toolProficiencies) + let armorTraining: [String]? = try values.decodeIfPresent([String].self, forKey: .armorTraining) + let startingEquipment: [[String]]? = try values.decodeIfPresent([[String]].self, forKey: .startingEquipment) + let experiencePoints = try values.decodeIfPresent([Int].self, forKey: .experiencePoints) // Safely set properties @@ -91,7 +136,15 @@ extension ClassTraits: Codable { self.descriptiveTraits = descriptiveTraits ?? [:] self.primaryAbility = primaryAbility ?? [] + self.alternatePrimaryAbility = alternatePrimaryAbility self.savingThrows = savingThrows ?? [] + self.startingSkillCount = startingSkillCount ?? 2 + self.skillProficiencies = skillProficiencies ?? [] + self.weaponProficiencies = weaponProficiencies ?? [] + self.toolProficiencies = toolProficiencies ?? [] + self.armorTraining = armorTraining ?? [] + self.startingEquipment = startingEquipment ?? [] + self.experiencePoints = experiencePoints } @@ -103,9 +156,17 @@ extension ClassTraits: Codable { try values.encode("\(hitDice)", forKey: .hitDice) try values.encode("\(startingWealth)", forKey: .startingWealth) - try values.encodeIfPresent(descriptiveTraits, forKey: .descriptiveTraits) - try values.encodeIfPresent(primaryAbility, forKey: .primaryAbility) - try values.encodeIfPresent(savingThrows, forKey: .savingThrows) + try values.encode(descriptiveTraits, forKey: .descriptiveTraits) + try values.encode(primaryAbility, forKey: .primaryAbility) + try values.encodeIfPresent(alternatePrimaryAbility, forKey: .alternatePrimaryAbility) + try values.encode(savingThrows, forKey: .savingThrows) + try values.encode(startingSkillCount, forKey: .startingSkillCount) + try values.encode(skillProficiencies, forKey: .skillProficiencies) + try values.encode(weaponProficiencies, forKey: .weaponProficiencies) + try values.encode(toolProficiencies, forKey: .toolProficiencies) + try values.encode(armorTraining, forKey: .armorTraining) + try values.encode(startingEquipment, forKey: .startingEquipment) + try values.encodeIfPresent(experiencePoints, forKey: .experiencePoints) } } diff --git a/RolePlayingCore/RolePlayingCore/Player/Initiative.swift b/RolePlayingCore/RolePlayingCore/Player/Initiative.swift index 590db89..e07f0be 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Initiative.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Initiative.swift @@ -6,6 +6,7 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // +// TODO: Make this public, add tests, etc. struct Initiative { let player: Player var value: Int diff --git a/RolePlayingCore/RolePlayingCore/Player/Player.swift b/RolePlayingCore/RolePlayingCore/Player/Player.swift index f213d91..4830b4f 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Player.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Player.swift @@ -34,9 +34,15 @@ public class Player: Codable { public var name: String public var descriptiveTraits: [String: String] // ideals, bonds, flaws, background + public private(set) var backgroundName: String public private(set) var speciesName: String public private(set) var className: String + public var backgroundTraits: BackgroundTraits! { + didSet { + self.backgroundName = backgroundTraits.name + } + } public var speciesTraits: SpeciesTraits! { didSet { self.speciesName = speciesTraits.name @@ -69,9 +75,21 @@ public class Player: Codable { /// Ability scores public var baseAbilities: AbilityScores - public var abilities: AbilityScores { baseAbilities + speciesTraits.abilityScoreIncrease } + public var backgroundAbilities: [Ability] + public var backgroundAbilityIncrease: AbilityScores { + var scores = AbilityScores() + for ability in backgroundAbilities { + scores[ability]! += 1 + } + return scores + } + + // TODO: limit adding backgroundAbilityIncrease to max score of 20 + public var abilities: AbilityScores { baseAbilities + backgroundAbilityIncrease } public var modifiers: AbilityScores { abilities.modifiers } + public var skills: [Skill] + /// Hit points, hit dice, experience points, and level public var maximumHitPoints: Int @@ -80,7 +98,7 @@ public class Player: Codable { public var level: Int public var speed: Int { speciesTraits.speed } - public var size: SpeciesTraits.Size { speciesTraits.size(from: height) } + public var size: SpeciesTraits.Size { SpeciesTraits.Size(from: height) } public var hitDice: Dice { classTraits.hitDice.hitDice(level: level) } @@ -100,6 +118,7 @@ public class Player: Codable { private enum CodingKeys: String, CodingKey { case name + case backgroundName = "background" case speciesName = "species" case className = "class" case descriptiveTraits = "descriptive traits" @@ -108,6 +127,8 @@ public class Player: Codable { case height case weight case baseAbilities = "ability scores" + case backgroundAbilities = "background ability scores" + case skills case maximumHitPoints = "maximum hit points" case currentHitPoints = "current hit points" case experiencePoints = "experience points" @@ -120,6 +141,7 @@ public class Player: Codable { // Try decoding properties let name = try values.decode(String.self, forKey: .name) + let backgroundName = try values.decode(String.self, forKey: .backgroundName) let speciesName = try values.decode(String.self, forKey: .speciesName) let className = try values.decode(String.self, forKey: .className) let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) @@ -128,6 +150,8 @@ public class Player: Codable { let height = try values.decode(Height.self, forKey: .height) let weight = try values.decode(Weight.self, forKey: .weight) let baseAbilities = try values.decode(AbilityScores.self, forKey: .baseAbilities) + let backgroundAbilities = try values.decode([String].self, forKey: .backgroundAbilities) + let skillNames = try values.decode([String].self, forKey: .skills) let maximumHitPoints = try values.decode(Int.self, forKey: .maximumHitPoints) let currentHitPoints = try values.decodeIfPresent(Int.self, forKey: .currentHitPoints) let experiencePoints = try values.decodeIfPresent(Int.self, forKey: .experiencePoints) @@ -136,6 +160,7 @@ public class Player: Codable { // Safely set properties self.name = name + self.backgroundName = backgroundName self.speciesName = speciesName self.className = className self.descriptiveTraits = descriptiveTraits ?? [:] @@ -144,6 +169,8 @@ public class Player: Codable { self.height = height self.weight = weight self.baseAbilities = baseAbilities + self.backgroundAbilities = backgroundAbilities.map { Ability($0) } + self.skills = Skill.skills(from: skillNames) self.maximumHitPoints = maximumHitPoints self.currentHitPoints = currentHitPoints ?? maximumHitPoints self.experiencePoints = experiencePoints ?? 0 @@ -156,6 +183,7 @@ public class Player: Codable { // Try decoding properties try values.encode(name, forKey: .name) + try values.encode(backgroundName, forKey: .backgroundName) try values.encode(speciesName, forKey: .speciesName) try values.encode(className, forKey: .className) try values.encodeIfPresent(descriptiveTraits, forKey: .descriptiveTraits) @@ -164,6 +192,8 @@ public class Player: Codable { try values.encode("\(height)", forKey: .height) try values.encode("\(weight)", forKey: .weight) try values.encode(baseAbilities, forKey: .baseAbilities) + try values.encode(backgroundAbilities.map({ $0.name }), forKey: .backgroundAbilities) + try values.encode(skills.skillNames, forKey: .skills) try values.encode(maximumHitPoints, forKey: .maximumHitPoints) try values.encodeIfPresent(currentHitPoints, forKey: .currentHitPoints) try values.encodeIfPresent(experiencePoints, forKey: .experiencePoints) @@ -172,11 +202,13 @@ public class Player: Codable { } // Creates a player character. - public init(_ name: String, speciesTraits: SpeciesTraits, classTraits: ClassTraits, gender: Gender? = nil, alignment: Alignment? = nil) { + public init(_ name: String, backgroundTraits: BackgroundTraits, speciesTraits: SpeciesTraits, classTraits: ClassTraits, gender: Gender? = nil, alignment: Alignment? = nil) { self.name = name self.descriptiveTraits = [:] + self.backgroundName = backgroundTraits.name self.speciesName = speciesTraits.name self.className = classTraits.name + self.backgroundTraits = backgroundTraits self.speciesTraits = speciesTraits self.classTraits = classTraits self.gender = gender @@ -190,6 +222,13 @@ public class Player: Codable { self.baseAbilities = AbilityScores() self.baseAbilities.roll() + + // TODO: roll for 2 or 3 background abilities, and if 2, add one random ability score twice + self.backgroundAbilities = backgroundTraits.abilityScores.map { Ability($0) } + + let skillProficiencies = Skill.skills(from: classTraits.skillProficiencies) + self.skills = skillProficiencies.randomSkills(count: classTraits.startingSkillCount) + self.skills.append(backgroundTraits.skillProficiencies) self.maximumHitPoints = Player.rollHitPoints(classTraits: classTraits, speciesTraits: speciesTraits) self.currentHitPoints = self.maximumHitPoints @@ -231,6 +270,7 @@ extension Player: Hashable { public static func == (lhs: Player, rhs: Player) -> Bool { return lhs.name == rhs.name && + lhs.backgroundName == rhs.backgroundName && lhs.speciesName == rhs.speciesName && lhs.className == rhs.className && lhs.descriptiveTraits == rhs.descriptiveTraits && @@ -248,6 +288,7 @@ extension Player: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(name) + hasher.combine(backgroundName) hasher.combine(speciesName) hasher.combine(className) hasher.combine(gender) diff --git a/RolePlayingCore/RolePlayingCore/Player/Players.swift b/RolePlayingCore/RolePlayingCore/Player/Players.swift index 0e9a326..3ec5cd3 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Players.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Players.swift @@ -9,7 +9,15 @@ import Foundation extension Player { - + + // TODO: support KeyedArchiver? + func resolveBackgrounds(from backgrounds: Backgrounds) throws { + guard let backgroundTraits = backgrounds.find(self.backgroundName) else { + throw RuntimeError("Could not resolve background name \(self.backgroundName)") + } + self.backgroundTraits = backgroundTraits + } + // TODO: support KeyedArchiver? func resolveSpecies(from species: Species) throws { guard let speciesTraits = species.find(self.speciesName) else { @@ -32,8 +40,9 @@ public class Players: Codable { public var players = [Player]() - public func resolve(classes: Classes, species: Species) throws { + public func resolve(backgrounds: Backgrounds, classes: Classes, species: Species) throws { for player in players { + try player.resolveBackgrounds(from: backgrounds) try player.resolveSpecies(from: species) try player.resolveClass(from: classes) } diff --git a/RolePlayingCore/RolePlayingCore/Player/Skill.swift b/RolePlayingCore/RolePlayingCore/Player/Skill.swift new file mode 100644 index 0000000..7dcecac --- /dev/null +++ b/RolePlayingCore/RolePlayingCore/Player/Skill.swift @@ -0,0 +1,73 @@ +// +// Skill.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/26/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +public struct Skill { + public let name: String + public let ability: Ability +} + +extension Skill: Codable { } + +extension Skill: Hashable { } + +extension Skill { + public static let acrobatics = Skill(name: "Acrobatics", ability: .dexterity) + public static let animalHandling = Skill(name: "Animal Handling", ability: .wisdom) + public static let arcana = Skill(name: "Arcana", ability: .intelligence) + public static let athletics = Skill(name: "Athletics", ability: .strength) + public static let deception = Skill(name: "Deception", ability: .charisma) + public static let history = Skill(name: "History", ability: .intelligence) + public static let insight = Skill(name: "Insight", ability: .wisdom) + public static let intimidation = Skill(name: "Intimidation", ability: .charisma) + public static let investigation = Skill(name: "Investigation", ability: .intelligence) + public static let medicine = Skill(name: "Medicine", ability: .wisdom) + public static let nature = Skill(name: "Nature", ability: .intelligence) + public static let perception = Skill(name: "Perception", ability: .wisdom) + public static let performance = Skill(name: "Performance", ability: .charisma) + public static let persuasion = Skill(name: "Persuasion", ability: .charisma) + public static let religion = Skill(name: "Religion", ability: .intelligence) + public static let sleightOfHand = Skill(name: "Sleight of Hand", ability: .dexterity) + public static let stealth = Skill(name: "Stealth", ability: .dexterity) + public static let survival = Skill(name: "Survival", ability: .wisdom) + + public static var all: [Skill] { + [ + .acrobatics, .animalHandling, .arcana, .athletics, .deception, .history, .insight, .intimidation, .investigation, .medicine, .nature, .perception, .performance, .persuasion, .religion, .sleightOfHand, .stealth, .survival + ] + } + + public static func skills(from names: [String]) -> [Skill] { + // Use the full set of skills if the names are empty. + guard names.count > 0 else { return all } + return all.filter { names.contains($0.name) } + } +} + +extension Sequence where Element == Skill { + + /// Returns a random array of skills with the specified skill count. + public func randomSkills(count: Int) -> [Element] { + var selected: [Element] = [] + var remaining: [Element] = Array(self) + + for _ in 0.. Size { - let heightInFeet = height.converted(to: .feet) - switch heightInFeet.value { - case 0..<4: - return .small - case 4..<7: - return .medium - default: - return .large + case huge + case gargantuan + + init(from height: Height) { + let heightInFeet = height.converted(to: .feet) + switch heightInFeet.value { + case 0..<4: + self = .small + case 4..<7: + self = .medium + default: + self = .large + } + } + + /// Space required in feet (dimension feet x feet) + var space: Double { + switch self { + case .tiny: return 2.5 + case .small, .medium: return 5.0 + case .large: return 10.0 + case .huge: return 15.0 + case .gargantuan: return 20.0 + } + } + + /// Space required in squares + var squares: Double { + switch self { + case .tiny: return 0.25 + case .small, .medium: return 1.0 + case .large: return 4.0 + case .huge: return 9.0 + case .gargantuan: return 16.0 + } } } - public var size: Size { size(from: baseHeight) } + public var size: Size { Size(from: baseHeight) } public init(name: String, plural: String, aliases: [String] = [], descriptiveTraits: [String: String] = [:], - abilityScoreIncrease: AbilityScores = AbilityScores(), minimumAge: Int, lifespan: Int, alignment: Alignment? = nil, @@ -68,7 +91,6 @@ public struct SpeciesTraits { self.plural = plural self.aliases = aliases self.descriptiveTraits = descriptiveTraits - self.abilityScoreIncrease = abilityScoreIncrease self.minimumAge = minimumAge self.lifespan = lifespan self.alignment = alignment @@ -89,7 +111,6 @@ extension SpeciesTraits: Codable { case plural case aliases case descriptiveTraits = "descriptive traits" - case abilityScoreIncrease = "ability scores" case minimumAge = "minimum age" case lifespan case alignment @@ -111,7 +132,6 @@ extension SpeciesTraits: Codable { let plural = try values.decode(String.self, forKey: .plural) let aliases = try values.decodeIfPresent([String].self, forKey: .aliases) let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) - let abilityScoreIncrease = try values.decodeIfPresent(AbilityScores.self, forKey: .abilityScoreIncrease) let minimumAge = try values.decodeIfPresent(Int.self, forKey: .minimumAge) let lifespan = try values.decodeIfPresent(Int.self, forKey: .lifespan) let alignment = try values.decodeIfPresent(Alignment.self, forKey: .alignment) @@ -128,7 +148,6 @@ extension SpeciesTraits: Codable { self.plural = plural self.aliases = aliases ?? [] self.descriptiveTraits = descriptiveTraits ?? [:] - self.abilityScoreIncrease = abilityScoreIncrease ?? AbilityScores() self.minimumAge = minimumAge self.lifespan = lifespan self.alignment = alignment @@ -157,9 +176,6 @@ extension SpeciesTraits: Codable { // The rest may be inherited from the parent. self.parentName = parent.name - // Combine ability scores together - self.abilityScoreIncrease += parent.abilityScoreIncrease - if self.minimumAge == nil { self.minimumAge = parent.minimumAge } @@ -199,7 +215,6 @@ extension SpeciesTraits: Codable { try values.encode(plural, forKey: .plural) try values.encode(aliases, forKey: .aliases) try values.encode(descriptiveTraits, forKey: .descriptiveTraits) - try values.encode(abilityScoreIncrease, forKey: .abilityScoreIncrease) try values.encode(minimumAge, forKey: .minimumAge) try values.encode(lifespan, forKey: .lifespan) @@ -236,12 +251,6 @@ extension SpeciesTraits: Codable { try values.encode(descriptiveTraits, forKey: .descriptiveTraits) } - // Un-combine ability scores - if self.abilityScoreIncrease != parent.abilityScoreIncrease { - let delta = self.abilityScoreIncrease - parent.abilityScoreIncrease - try values.encode(delta, forKey: .abilityScoreIncrease) - } - if self.minimumAge != parent.minimumAge { try values.encode(self.minimumAge, forKey: .minimumAge) } diff --git a/RolePlayingCore/RolePlayingCoreTests/BackgroundsTests.swift b/RolePlayingCore/RolePlayingCoreTests/BackgroundsTests.swift new file mode 100644 index 0000000..f39ce62 --- /dev/null +++ b/RolePlayingCore/RolePlayingCoreTests/BackgroundsTests.swift @@ -0,0 +1,149 @@ +// +// BackgroundsTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/27/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +import XCTest +import RolePlayingCore + +class BackgroundsTests: XCTestCase { + + func testBackgroundTraitsDecoding() throws { + // Given: JSON data representing a background + let jsonData = """ + { + "name": "Acolyte", + "ability scores": ["Intelligence", "Wisdom"], + "feat": "Magic Initiate", + "skill proficiencies": ["Insight", "Religion"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], ["15 GP"]] + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + + // When: Decoding the JSON data + let background = try decoder.decode(BackgroundTraits.self, from: jsonData) + + // Then: The properties should match the input + XCTAssertEqual(background.name, "Acolyte", "Name should match") + XCTAssertEqual(background.abilityScores, ["Intelligence", "Wisdom"], "Ability scores should match") + XCTAssertEqual(background.feat, "Magic Initiate", "Feat should match") + XCTAssertEqual(background.skillProficiencies.count, 2, "Should have 2 skill proficiencies") + XCTAssertEqual(background.skillProficiencies.skillNames, ["Insight", "Religion"], "Skill names should match") + XCTAssertEqual(background.toolProficiency, "Calligrapher's Supplies", "Tool proficiency should match") + XCTAssertEqual(background.equipment.count, 2, "Should have 2 equipment choices") + XCTAssertEqual(background.equipment[0], ["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], "First equipment choice should match") + } + + func testBackgroundTraitsEncoding() throws { + // Given: A BackgroundTraits instance + let jsonData = """ + { + "name": "Criminal", + "ability scores": ["Dexterity", "Intelligence"], + "feat": "Alert", + "skill proficiencies": ["Deception", "Stealth"], + "tool proficiency": "Thieves' Tools", + "equipment": [["Crowbar", "Dark Clothes", "Thieves' Tools", "16 GP"]] + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let background = try decoder.decode(BackgroundTraits.self, from: jsonData) + + // When: Encoding the background back to JSON + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let encodedData = try encoder.encode(background) + + // Then: The encoded data should be decodable and match the original + let decodedBackground = try decoder.decode(BackgroundTraits.self, from: encodedData) + + XCTAssertEqual(decodedBackground.name, background.name, "Name should match after round-trip") + XCTAssertEqual(decodedBackground.abilityScores, background.abilityScores, "Ability scores should match after round-trip") + XCTAssertEqual(decodedBackground.feat, background.feat, "Feat should match after round-trip") + XCTAssertEqual(decodedBackground.skillProficiencies.skillNames, background.skillProficiencies.skillNames, "Skills should match after round-trip") + XCTAssertEqual(decodedBackground.toolProficiency, background.toolProficiency, "Tool proficiency should match after round-trip") + XCTAssertEqual(decodedBackground.equipment, background.equipment, "Equipment should match after round-trip") + } + + func testBackgroundsCollection() throws { + // Given: JSON data representing a collection of backgrounds + let jsonData = """ + { + "backgrounds": [ + { + "name": "Acolyte", + "ability scores": ["Intelligence", "Wisdom"], + "feat": "Magic Initiate", + "skill proficiencies": ["Insight", "Religion"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], ["15 GP"]] + }, + { + "name": "Criminal", + "ability scores": ["Dexterity", "Intelligence"], + "feat": "Alert", + "skill proficiencies": ["Deception", "Stealth"], + "tool proficiency": "Thieves' Tools", + "equipment": [["Crowbar", "Dark Clothes", "Thieves' Tools", "16 GP"]] + }, + { + "name": "Soldier", + "ability scores": ["Strength", "Constitution"], + "feat": "Savage Attacker", + "skill proficiencies": ["Athletics", "Intimidation"], + "tool proficiency": "Gaming Set", + "equipment": [["Spear", "Shortbow", "20 Arrows", "Quiver", "Uniform", "16 GP"]] + } + ] + } + """.data(using: .utf8)! + + let decoder = JSONDecoder() + + // When: Decoding the JSON data into a Backgrounds collection + let backgrounds = try decoder.decode(Backgrounds.self, from: jsonData) + + // Then: The collection should have the correct count + XCTAssertEqual(backgrounds.count, 3, "Should have 3 backgrounds") + + // Then: The find method should locate backgrounds by name + let acolyte = backgrounds.find("Acolyte") + XCTAssertNotNil(acolyte, "Should find Acolyte background") + XCTAssertEqual(acolyte?.name, "Acolyte", "Found background should be Acolyte") + XCTAssertEqual(acolyte?.feat, "Magic Initiate", "Acolyte feat should match") + + let criminal = backgrounds.find("Criminal") + XCTAssertNotNil(criminal, "Should find Criminal background") + XCTAssertEqual(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") + XCTAssertNil(nonExistent, "Should not find non-existent background") + + // Then: Subscript access should work correctly + let firstBackground = backgrounds[0] + XCTAssertNotNil(firstBackground, "Should access first background") + XCTAssertEqual(firstBackground?.name, "Acolyte", "First background should be Acolyte") + + let thirdBackground = backgrounds[2] + XCTAssertNotNil(thirdBackground, "Should access third background") + XCTAssertEqual(thirdBackground?.name, "Soldier", "Third background should be Soldier") + + // Then: Round-trip encoding should preserve data + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + let encodedData = try encoder.encode(backgrounds) + let decodedBackgrounds = try decoder.decode(Backgrounds.self, from: encodedData) + + XCTAssertEqual(decodedBackgrounds.count, backgrounds.count, "Count should match after round-trip") + XCTAssertEqual(decodedBackgrounds.find("Criminal")?.name, "Criminal", "Should find Criminal after round-trip") + XCTAssertEqual(decodedBackgrounds[1]?.toolProficiency, backgrounds[1]?.toolProficiency, "Tool proficiency should match after round-trip") + } +} diff --git a/RolePlayingCore/RolePlayingCoreTests/ClassTraitsTests.swift b/RolePlayingCore/RolePlayingCoreTests/ClassTraitsTests.swift index f9440d8..c77f985 100644 --- a/RolePlayingCore/RolePlayingCoreTests/ClassTraitsTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/ClassTraitsTests.swift @@ -186,5 +186,220 @@ class ClassTraitsTests: XCTestCase { XCTAssertNil(classTraits) } } - + + // MARK: - Edge Case Tests + + func testExperiencePointsEdgeCases() { + // Test with empty experience points array + do { + let traits = """ + { + "name": "Novice", + "plural": "Novices", + "hit dice": "d6", + "starting wealth": "2d4x10", + "experience points": [] + } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + XCTAssertNotNil(classTraits.experiencePoints, "Should decode empty array") + XCTAssertEqual(classTraits.experiencePoints?.count, 0, "Should have 0 experience points") + XCTAssertEqual(classTraits.maxLevel, 0, "Max level should be 0 for empty array") + XCTAssertEqual(classTraits.minExperiencePoints(at: 1), 0, "Min XP should be 0") + XCTAssertEqual(classTraits.maxExperiencePoints(at: 1), -1, "Max XP should be -1") + } catch { + XCTFail("Failed to decode: \(error)") + } + + // Test with single level experience points + do { + let traits = """ + { + "name": "Novice", + "plural": "Novices", + "hit dice": "d6", + "starting wealth": "2d4x10", + "experience points": [0] + } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + XCTAssertEqual(classTraits.maxLevel, 1, "Max level should be 1") + XCTAssertEqual(classTraits.minExperiencePoints(at: 1), 0, "Min XP at level 1 should be 0") + XCTAssertEqual(classTraits.minExperiencePoints(at: 2), 0, "Beyond max level should return last value") + } catch { + XCTFail("Failed to decode: \(error)") + } + + // Test level 0 and negative levels + do { + let classTraits = ClassTraits( + name: "Test", + plural: "Tests", + hitDice: SimpleDice(.d8), + startingWealth: SimpleDice(.d4), + experiencePoints: [0, 300, 900, 2700] + ) + + XCTAssertEqual(classTraits.minExperiencePoints(at: 0), 0, "Level 0 should map to level 1") + XCTAssertEqual(classTraits.minExperiencePoints(at: -5), 0, "Negative level should map to level 1") + XCTAssertEqual(classTraits.maxExperiencePoints(at: 0), 0, "Max XP at level 0 should work") + } + + // Test beyond max level + do { + let classTraits = ClassTraits( + name: "Test", + plural: "Tests", + hitDice: SimpleDice(.d8), + startingWealth: SimpleDice(.d4), + experiencePoints: [0, 300, 900] + ) + + XCTAssertEqual(classTraits.maxLevel, 3, "Max level should be 3") + XCTAssertEqual(classTraits.minExperiencePoints(at: 10), 900, "Beyond max should return last value") + XCTAssertEqual(classTraits.maxExperiencePoints(at: 3), 899, "Max XP at level 3") + } + } + + func testEmptyAndNilOptionalFields() { + // Test with all optional fields as empty arrays/dictionaries + do { + let traits = """ + { + "name": "Minimalist", + "plural": "Minimalists", + "hit dice": "d8", + "starting wealth": "3d4x10", + "descriptive traits": {}, + "primary ability": [], + "saving throws": [], + "starting skill count": 0, + "skill proficiencies": [], + "weapon proficiencies": [], + "tool proficiencies": [], + "armor training": [], + "starting equipment": [] + } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + + XCTAssertEqual(classTraits.descriptiveTraits.count, 0, "Descriptive traits should be empty") + XCTAssertEqual(classTraits.primaryAbility.count, 0, "Primary ability should be empty") + XCTAssertNil(classTraits.alternatePrimaryAbility, "Alternate primary ability should be nil") + XCTAssertEqual(classTraits.savingThrows.count, 0, "Saving throws should be empty") + XCTAssertEqual(classTraits.startingSkillCount, 0, "Starting skill count should be 0") + XCTAssertEqual(classTraits.skillProficiencies.count, 0, "Skill proficiencies should be empty") + XCTAssertEqual(classTraits.weaponProficiencies.count, 0, "Weapon proficiencies should be empty") + XCTAssertEqual(classTraits.toolProficiencies.count, 0, "Tool proficiencies should be empty") + XCTAssertEqual(classTraits.armorTraining.count, 0, "Armor training should be empty") + XCTAssertEqual(classTraits.startingEquipment.count, 0, "Starting equipment should be empty") + XCTAssertNil(classTraits.experiencePoints, "Experience points should be nil") + } catch { + XCTFail("Failed to decode: \(error)") + } + } + + func testMultiplePrimaryAbilities() { + // Test with multiple primary and alternate abilities + do { + let traits = """ + { + "name": "Ranger", + "plural": "Rangers", + "hit dice": "d10", + "starting wealth": "5d4x10", + "primary ability": ["Strength", "Dexterity"], + "alternate primary ability": ["Constitution", "Wisdom"] + } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + + XCTAssertEqual(classTraits.primaryAbility.count, 2, "Should have 2 primary abilities") + XCTAssertEqual(classTraits.primaryAbility, [Ability("Strength"), Ability("Dexterity")]) + XCTAssertEqual(classTraits.alternatePrimaryAbility?.count, 2, "Should have 2 alternate abilities") + XCTAssertEqual(classTraits.alternatePrimaryAbility, [Ability("Constitution"), Ability("Wisdom")]) + } catch { + XCTFail("Failed to decode: \(error)") + } + } + + func testNestedStartingEquipment() { + // Test with complex nested equipment choices + do { + let traits = """ + { + "name": "Paladin", + "plural": "Paladins", + "hit dice": "d10", + "starting wealth": "5d4x10", + "starting equipment": [ + ["Longsword", "Shield"], + ["Greatsword"], + ["5 Javelins", "Simple Weapon"], + ["Priest's Pack", "Explorer's Pack"], + ["Chain Mail", "Holy Symbol"] + ] + } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + + XCTAssertEqual(classTraits.startingEquipment.count, 5, "Should have 5 equipment choices") + XCTAssertEqual(classTraits.startingEquipment[0], ["Longsword", "Shield"]) + XCTAssertEqual(classTraits.startingEquipment[1], ["Greatsword"]) + XCTAssertEqual(classTraits.startingEquipment[3], ["Priest's Pack", "Explorer's Pack"]) + } catch { + XCTFail("Failed to decode: \(error)") + } + } + + func testRoundTripEncodingWithAllFields() { + // Test complete round-trip encoding/decoding with all fields populated + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + let original = ClassTraits( + name: "Bard", + plural: "Bards", + hitDice: SimpleDice(.d8), + startingWealth: CompoundDice(.d4, times: 5, modifier: 10, mathOperator: "x"), + descriptiveTraits: ["Spellcasting": "Can cast spells", "Bardic Inspiration": "Can inspire others"], + primaryAbility: [Ability("Charisma")], + alternatePrimaryAbility: [Ability("Dexterity")], + savingThrows: [Ability("Dexterity"), Ability("Charisma")], + startingSkillCount: 3, + skillProficiencies: ["Acrobatics", "Performance", "Persuasion"], + weaponProficiencies: ["Simple Weapons", "Hand Crossbows", "Longswords", "Rapiers", "Shortswords"], + toolProficiencies: ["Three Musical Instruments"], + armorTraining: ["Light Armor"], + startingEquipment: [["Rapier", "Longsword"], ["Diplomat's Pack", "Entertainer's Pack"]], + experiencePoints: [0, 300, 900, 2700, 6500, 14000] + ) + + do { + let encoded = try encoder.encode(original) + let decoded = try decoder.decode(ClassTraits.self, from: encoded) + + XCTAssertEqual(decoded.name, original.name, "Name should match after round-trip") + XCTAssertEqual(decoded.plural, original.plural, "Plural should match") + XCTAssertEqual(decoded.primaryAbility, original.primaryAbility, "Primary ability should match") + XCTAssertEqual(decoded.alternatePrimaryAbility, original.alternatePrimaryAbility, "Alternate ability should match") + XCTAssertEqual(decoded.savingThrows, original.savingThrows, "Saving throws should match") + XCTAssertEqual(decoded.startingSkillCount, original.startingSkillCount, "Skill count should match") + XCTAssertEqual(decoded.skillProficiencies, original.skillProficiencies, "Skill proficiencies should match") + XCTAssertEqual(decoded.weaponProficiencies, original.weaponProficiencies, "Weapon proficiencies should match") + XCTAssertEqual(decoded.toolProficiencies, original.toolProficiencies, "Tool proficiencies should match") + XCTAssertEqual(decoded.armorTraining, original.armorTraining, "Armor training should match") + XCTAssertEqual(decoded.startingEquipment, original.startingEquipment, "Starting equipment should match") + XCTAssertEqual(decoded.experiencePoints, original.experiencePoints, "Experience points should match") + XCTAssertEqual(decoded.descriptiveTraits.count, original.descriptiveTraits.count, "Descriptive traits count should match") + XCTAssertEqual(decoded.maxLevel, 6, "Max level should be 6") + } catch { + XCTFail("Failed round-trip encoding/decoding: \(error)") + } + } } diff --git a/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift b/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift index 8d7a23c..7cd5bc2 100644 --- a/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift @@ -22,7 +22,6 @@ class ConfigurationTests: XCTestCase { let abilities = Ability.defaults for ability in abilities { var importantFor = [String]() - var speciesIncreases = [String]() for classTraits in configuration.classes.classes { if classTraits.primaryAbility.contains(ability) { @@ -30,21 +29,11 @@ class ConfigurationTests: XCTestCase { } } - for speciesTraits in configuration.species.species { - if let increase = speciesTraits.abilityScoreIncrease[ability], increase != 0 { - - speciesIncreases.append("\(speciesTraits.name) (+\(increase))") - } - } - // TODO: make into assertions. For now, visually compare results. print(ability.name) let important = importantFor.count == 0 ? ["Everyone"] : importantFor print("Important for: \(important)") - print("Species increases: \(speciesIncreases)") } - - } catch let error { XCTFail("Configuration threw an error: \(error)") diff --git a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift index 1bc8dac..e875c94 100644 --- a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift @@ -12,6 +12,8 @@ import RolePlayingCore class PlayerTests: XCTestCase { + var soldierTraits: Data! + var soldier: BackgroundTraits! var humanTraits: Data! var human: SpeciesTraits! var fighterTraits: Data! @@ -25,6 +27,18 @@ class PlayerTests: XCTestCase { let data = try! bundle.loadJSON("TestCurrencies") _ = try! decoder.decode(Currencies.self, from: data) + self.soldierTraits = """ + { + "name": "Soldier", + "ability scores": ["Strength", "Dexterity", "Constitution"], + "feat": "Savage Attacker", + "skill proficiencies" : ["Athletics", "Intimidation"], + "tool proficiency": "Gaming Set", + "equipment": [["Spear", "Shortbow", "20 Arrows", "Gaming Set", "Healer's Kit", "Quiver", "Traveler's Clothes", "14 GP"], ["50 GP"]] + } + """.data(using: .utf8) + self.soldier = try! decoder.decode(BackgroundTraits.self, from: self.soldierTraits) + self.fighterTraits = """ { "name": "Fighter", @@ -43,7 +57,6 @@ class PlayerTests: XCTestCase { { "name": "Human", "plural": "Humans", - "ability scores": {"Strength": 1, "Dexterity": 1, "Constitution": 1, "Intelligence": 1, "Wisdom": 1, "Charisma": 1}, "minimum age": 18, "lifespan": 90, "base height": "4'8\\"", @@ -63,7 +76,7 @@ class PlayerTests: XCTestCase { // Test construction from types do { - let player = Player("Frodo", speciesTraits: human, classTraits: fighter, gender: .female, alignment: Alignment(.lawful, .neutral)) + let player = Player("Frodo", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .female, alignment: Alignment(.lawful, .neutral)) XCTAssertEqual(player.name, "Frodo", "player name") XCTAssertEqual(player.className, "Fighter", "class name") XCTAssertEqual(player.speciesName, "Human", "species name") @@ -101,12 +114,15 @@ class PlayerTests: XCTestCase { let playerTraits = """ { "name": "Bilbo", + "background": "Sailor", "species": "Human", "class": "Fighter", "gender": "Male", "height": "3'9\\"", "weight": 120, - "ability scores": {"Dexterity": 13}, + "ability scores": {"Dexterity": 13, "Charisma": 12}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skills": ["Athletics"], "money": 130, "maximum hit points": 10 } @@ -124,6 +140,9 @@ class PlayerTests: XCTestCase { XCTAssertEqual(player.gender, Player.Gender.male, "gender") XCTAssertNil(player.alignment, "alignment") + XCTAssertEqual(player.abilities[.dexterity], 14, "dexterity") + XCTAssertEqual(player.abilities[.charisma], 12, "charisma") + XCTAssertEqual(player.height.value, 3.75, "height") XCTAssertEqual(player.weight.value, 120, "weight") @@ -146,12 +165,15 @@ class PlayerTests: XCTestCase { let playerTraits = """ { "name": "Bilbo", + "background": "Sailor", "species": "Human", "class": "Fighter", "alignment": "Lawful Evil", "height": "3'9\\"", "weight": 120, "ability scores": {"Strength": 12}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skills": ["Athletics"], "money": 130, "maximum hit points": 10, "experience points": 2300, @@ -189,6 +211,7 @@ class PlayerTests: XCTestCase { let playerTraits = """ { "name": "Bilbo", + "background": "Sailor", "species": "Human", "class": "Fighter", "gender": "Male", @@ -196,6 +219,8 @@ class PlayerTests: XCTestCase { "height": "3'9\\"", "weight": 120, "ability scores": {"Dexterity": 13}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skills": ["Athletics"], "money": 130, "maximum hit points": 20, "current hit points": 9, @@ -226,6 +251,12 @@ class PlayerTests: XCTestCase { print("\(String(describing: abilities))") XCTAssertEqual(abilities?["Dexterity"], 13, "player traits round trip ability scores") + let backgroundAbilities = encoded["background ability scores"] as? [String] + XCTAssertNotNil(backgroundAbilities) + XCTAssertEqual(backgroundAbilities?.count, 3, "player traits round trip background ability scores count") + XCTAssertTrue(backgroundAbilities!.contains("Strength"), "player traits round trip background ability scores") + XCTAssertFalse(backgroundAbilities!.contains("Charisma"), "player traits round trip background ability scores") + XCTAssertEqual(encoded["money"] as? String, "130.0 gp", "player traits round trip money") XCTAssertEqual(encoded["maximum hit points"] as? Int, 20, "player traits round trip maximum hit points") XCTAssertEqual(encoded["current hit points"] as? Int, 9, "player traits round trip current hit points") @@ -313,10 +344,11 @@ class PlayerTests: XCTestCase { } func testComputedProperties() { - let player = Player("Gandalf", speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.neutral, .good)) + let player = Player("Gandalf", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.neutral, .good)) // Test speed (from species traits) XCTAssertEqual(player.speed, 30, "speed should match species speed") + XCTAssertEqual(player.size, .medium, "size should match species size") // Test modifiers for ability in player.modifiers.abilities { @@ -334,7 +366,7 @@ class PlayerTests: XCTestCase { } func testProficiencyBonus() { - let player = Player("Aragorn", speciesTraits: human, classTraits: fighter) + let player = Player("Aragorn", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) // Level 1-4: +2 player.level = 1 @@ -373,7 +405,7 @@ class PlayerTests: XCTestCase { } func testHitDiceAtDifferentLevels() { - let player = Player("Legolas", speciesTraits: human, classTraits: fighter) + let player = Player("Legolas", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) player.level = 1 XCTAssertEqual("\(player.hitDice)", "d10", "hit dice at level 1") @@ -389,10 +421,10 @@ class PlayerTests: XCTestCase { } func testHashableConformance() { - let player1 = Player("Gimli", speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) + let player1 = Player("Gimli", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) player1.descriptiveTraits = ["ideal": "Honor", "bond": "My axe"] - let player2 = Player("Gimli", speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) + let player2 = Player("Gimli", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) player2.speciesTraits = human player2.classTraits = fighter player2.baseAbilities = player1.baseAbilities @@ -424,14 +456,14 @@ class PlayerTests: XCTestCase { } func testPlayerInequality() { - let player1 = Player("Boromir", speciesTraits: human, classTraits: fighter) - let player2 = Player("Faramir", speciesTraits: human, classTraits: fighter) + let player1 = Player("Boromir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + let player2 = Player("Faramir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) // Different names XCTAssertNotEqual(player1, player2, "players with different names should not be equal") // Different hit points - let player3 = Player("Boromir", speciesTraits: human, classTraits: fighter) + let player3 = Player("Boromir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) player3.baseAbilities = player1.baseAbilities player3.height = player1.height player3.weight = player1.weight @@ -443,18 +475,18 @@ class PlayerTests: XCTestCase { func testGenderCases() { // Test all gender cases - let female = Player("Diana", speciesTraits: human, classTraits: fighter, gender: .female) + let female = Player("Diana", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .female) XCTAssertEqual(female.gender, .female, "female gender") - let male = Player("Arthur", speciesTraits: human, classTraits: fighter, gender: .male) + let male = Player("Arthur", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male) XCTAssertEqual(male.gender, .male, "male gender") - let agender = Player("Riley", speciesTraits: human, classTraits: fighter, gender: nil) + let agender = Player("Riley", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: nil) XCTAssertNil(agender.gender, "nil gender for androgynous/hermaphroditic") } func testDescriptiveTraits() { - let player = Player("Samwise", speciesTraits: human, classTraits: fighter) + let player = Player("Samwise", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) // Initially empty XCTAssertEqual(player.descriptiveTraits.count, 0) @@ -501,8 +533,9 @@ class PlayerTests: XCTestCase { } func testSpeciesAndClassTraitsDidSet() { - let player = Player("Test", speciesTraits: human, classTraits: fighter) + let player = Player("Test", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) + XCTAssertEqual(player.backgroundName, "Soldier") XCTAssertEqual(player.speciesName, "Human") XCTAssertEqual(player.className, "Fighter") @@ -524,6 +557,7 @@ class PlayerTests: XCTestCase { let playerTraits = """ { "name": "Pippin", + "background": "Sailor", "species": "Human", "class": "Fighter", "descriptive traits": { @@ -534,6 +568,8 @@ class PlayerTests: XCTestCase { "height": "4'2\\"", "weight": 95, "ability scores": {"Charisma": 14, "Dexterity": 15}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skills": ["Athletics"], "money": 100, "maximum hit points": 12 } @@ -573,11 +609,14 @@ class PlayerTests: XCTestCase { let playerTraits = """ { "name": "Merry", + "background": "Sailor", "species": "Human", "class": "Fighter", "height": "4'2\\"", "weight": 95, "ability scores": {"Strength": 14}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skills": ["Athletics"], "money": 100, "maximum hit points": 12, "experience points": 0, diff --git a/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift b/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift index 540d47f..e74f96b 100644 --- a/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift @@ -15,6 +15,7 @@ class PlayersTests: XCTestCase { let bundle = Bundle(for: PlayersTests.self) let decoder = JSONDecoder() + var backgrounds: Backgrounds! var classes: Classes! var species: Species! @@ -23,6 +24,9 @@ class PlayersTests: XCTestCase { let currenciesData = try! bundle.loadJSON("TestCurrencies") _ = try! decoder.decode(Currencies.self, from: currenciesData) + let backgroundsData = try! bundle.loadJSON("TestBackgrounds") + backgrounds = try! decoder.decode(Backgrounds.self, from: backgroundsData) + let classesData = try! bundle.loadJSON("TestClasses") classes = try! decoder.decode(Classes.self, from: classesData) @@ -36,7 +40,7 @@ class PlayersTests: XCTestCase { do { let playersData = try bundle.loadJSON("TestPlayers") players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) + try players.resolve(backgrounds: backgrounds, classes: classes, species: species) } catch let error { XCTFail("players.load failed, error \(error)") @@ -57,7 +61,7 @@ class PlayersTests: XCTestCase { do { let playersData = try! bundle.loadJSON("InvalidClassPlayers") let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) + try players.resolve(backgrounds: backgrounds, classes: classes, species: species) XCTFail("players.load should have failed") } catch let error { @@ -67,7 +71,7 @@ class PlayersTests: XCTestCase { do { let playersData = try! bundle.loadJSON("InvalidSpeciesPlayers") let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) + try players.resolve(backgrounds: backgrounds, classes: classes, species: species) XCTFail("players.resolve should have failed") } catch let error { @@ -77,7 +81,7 @@ class PlayersTests: XCTestCase { do { let playersData = try! bundle.loadJSON("MissingClassPlayers") let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) + try players.resolve(backgrounds: backgrounds, classes: classes, species: species) XCTFail("players.resolve should have failed") } catch let error { @@ -87,7 +91,7 @@ class PlayersTests: XCTestCase { do { let playersData = try! bundle.loadJSON("MissingSpeciesPlayers") let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(classes: classes, species: species) + try players.resolve(backgrounds: backgrounds, classes: classes, species: species) XCTFail("players.resolve should have failed") } diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift index 8aa5853..4cce908 100644 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift @@ -21,7 +21,6 @@ class SpeciesTraitsTests: XCTestCase { { "name": "Human", "plural": "Humans", - "ability scores": {"Strength": 1, "Dexterity": 1, "Constitution": 1, "Intelligence": 1, "Wisdom": 1, "Charisma": 1}, "minimum age": 18, "lifespan": 90, "base height": "4'8\\"", @@ -44,13 +43,7 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertNotNil(speciesTraits) XCTAssertEqual(speciesTraits?.name, "Human", "name") XCTAssertEqual(speciesTraits?.plural, "Humans", "plural") - XCTAssertEqual(speciesTraits?.abilityScoreIncrease.count, 6, "ability score increase") - if let scores = speciesTraits?.abilityScoreIncrease.values { - for score in scores { - XCTAssertEqual(score, 1, "ability score increase") - } - } - + XCTAssertEqual(speciesTraits?.minimumAge, 18, "minimum age") XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") XCTAssertEqual(speciesTraits?.baseHeight.value ?? 0, 4.666666, accuracy: 0.000001, "base height") @@ -99,12 +92,6 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertNotNil(speciesTraits) XCTAssertEqual(speciesTraits?.name, "Giant Human", "name") XCTAssertEqual(speciesTraits?.plural, "Giant Humans", "plural") - XCTAssertEqual(speciesTraits?.abilityScoreIncrease.count, 6, "ability score increase") - if let scores = speciesTraits?.abilityScoreIncrease.values { - for score in scores { - XCTAssertEqual(score, 0, "ability score increase") - } - } XCTAssertEqual(speciesTraits?.minimumAge, 18, "minimum age") XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") @@ -224,10 +211,6 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertEqual(subspeciesTraits.name, "Subhuman", "name") XCTAssertEqual(subspeciesTraits.plural, "Subhumans", "plural") - XCTAssertEqual(subspeciesTraits.abilityScoreIncrease.count, 6, "ability score increase") - for score in subspeciesTraits.abilityScoreIncrease.values { - XCTAssertEqual(score, 0, "ability score increase") - } XCTAssertEqual(subspeciesTraits.minimumAge, 15, "minimum age") XCTAssertEqual(subspeciesTraits.lifespan, 60, "lifespan") @@ -278,7 +261,6 @@ class SpeciesTraitsTests: XCTestCase { "plural": "Folks", "aliases": ["Plainfolk"], "weight modifier": "d8", - "ability scores": {"Strength": 2, "Dexterity": 1, "Constitution": 3, "Intelligence": 2, "Wisdom": 1, "Charisma": 1}, "alignment": "Neutral", "darkvision": 20, "hit point bonus": 2 @@ -293,10 +275,6 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertNotNil(subspeciesTraits) XCTAssertEqual(subspeciesTraits.name, "Folk", "name") XCTAssertEqual(subspeciesTraits.plural, "Folks", "plural") - XCTAssertEqual(subspeciesTraits.abilityScoreIncrease.count, 6, "ability score increase") - for score in subspeciesTraits.abilityScoreIncrease.values { - XCTAssertNotEqual(score, 0, "ability score increase") - } XCTAssertEqual(subspeciesTraits.minimumAge, 18, "minimum age") XCTAssertEqual(subspeciesTraits.lifespan, 90, "lifespan") @@ -334,7 +312,7 @@ class SpeciesTraitsTests: XCTestCase { } func testEncodingSubspeciesTraits() { - let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], descriptiveTraits: [:], abilityScoreIncrease: AbilityScores(), minimumAge: 18, lifespan: 90, alignment: Alignment(.lawful, .neutral), baseHeight: "4ft 9 in".parseHeight!, heightModifier: DiceModifier(0), baseWeight: "178 lb".parseWeight!, weightModifier: DiceModifier(0), darkVision: 0, speed: 45, hitPointBonus: 0) + let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], descriptiveTraits: [:], minimumAge: 18, lifespan: 90, alignment: Alignment(.lawful, .neutral), baseHeight: "4ft 9 in".parseHeight!, heightModifier: DiceModifier(0), baseWeight: "178 lb".parseWeight!, weightModifier: DiceModifier(0), darkVision: 0, speed: 45, hitPointBonus: 0) let encoder = JSONEncoder() @@ -390,7 +368,7 @@ class SpeciesTraitsTests: XCTestCase { do { var copyOfSpeciesTraits = speciesTraits - let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], descriptiveTraits: ["background": "Something"], abilityScoreIncrease: AbilityScores([Ability("Strength"): 2]), minimumAge: 14, lifespan: 45, alignment: Alignment(.neutral, .evil), baseHeight: "3 ft".parseHeight!, heightModifier: "d4".parseDice!, baseWeight: "100 lb".parseWeight!, weightModifier: "d6".parseDice!, darkVision: 10, speed: 45, hitPointBonus: 1) + let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], descriptiveTraits: ["background": "Something"], minimumAge: 14, lifespan: 45, alignment: Alignment(.neutral, .evil), baseHeight: "3 ft".parseHeight!, heightModifier: "d4".parseDice!, baseWeight: "100 lb".parseWeight!, weightModifier: "d6".parseDice!, darkVision: 10, speed: 45, hitPointBonus: 1) copyOfSpeciesTraits.subspecies.append(subspeciesTraits) let encoded = try encoder.encode(copyOfSpeciesTraits) diff --git a/RolePlayingCore/RolePlayingCoreTests/TestBackgrounds.json b/RolePlayingCore/RolePlayingCoreTests/TestBackgrounds.json new file mode 100644 index 0000000..e076e7e --- /dev/null +++ b/RolePlayingCore/RolePlayingCoreTests/TestBackgrounds.json @@ -0,0 +1,37 @@ +{ + "backgrounds": [ + { + "name": "Acolyte", + "ability scores": ["Intelligence", "Wisdom", "Charisma"], + "feat": "Magic Initiate (Cleric)", + "skill proficiencies" : ["Insight", "Religion"], + "tool proficiency": "Calligrapher's Supplies", + "equipment": [["Calligrapher's Supplies", "Book (prayers)", "Holy Symbol", "Parchment (10 sheets)", "Robe", "8 GP"], ["50 GP"]] + }, + { + "name": "Farmer", + "ability scores": ["Strength", "Constitution", "Wisdom"], + "feat": "Tough", + "skill proficiencies" : ["Animal Handling", "Nature"], + "tool proficiency": "Carpenter's Tools", + "equipment": [["Sickle", "Carpenter's Tools", "Healer's Kit", "Iron Pot", "Shovel", "Traveler's Clothes", "30 GP"], ["50 GP"]] + }, + { + "name": "Guide", + "ability scores": ["Dexterity", "Constitution", "Wisdom"], + "feat": "Magic Initiate (Druid)", + "skill proficiencies" : ["Stealth", "Survival"], + "tool proficiency": "Cartographer's Tools", + "equipment": [["Shortbow", "20 Arrows", "Cartographer's Tools", "Bedroll", "Quiver", "Tent", "Traveler's Clothes", "3 GP"], ["50 GP"]] + }, + { + "name": "Sailor", + "ability scores": ["Strength", "Dexterity", "Wisdom"], + "feat": "Tavern Brawler", + "skill proficiencies" : ["Acrobatics", "Perception"], + "tool proficiency": "Navigator's Tools", + "equipment": [["Dagger", "Navigator's Tools", "Rope", "Traveler's Clothes", "20 GP"], ["50 GP"]] + } + ] +} + diff --git a/RolePlayingCore/RolePlayingCoreTests/TestCharacterGenerator.json b/RolePlayingCore/RolePlayingCoreTests/TestCharacterGenerator.json index e833c7d..81dcda4 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestCharacterGenerator.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestCharacterGenerator.json @@ -1,5 +1,6 @@ { "currencies": ["TestCurrencies"], + "backgrounds": ["TestBackgrounds"], "classes": ["TestClasses", "TestMoreClasses"], "species": ["TestSpecies", "TestMoreSpecies"], "players": ["TestPlayers"], diff --git a/RolePlayingCore/RolePlayingCoreTests/TestClasses.json b/RolePlayingCore/RolePlayingCoreTests/TestClasses.json index 864c9f2..ff859b5 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestClasses.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestClasses.json @@ -28,8 +28,8 @@ "primary ability": ["Wisdom"], "saving throws": ["Wisdom", "Charisma"], "starting wealth": "5d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple"], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple"], }, { "name": "Fighter", @@ -39,8 +39,8 @@ "alternate primary ability": ["Dexterity"], "saving throws": ["Strength", "Constitution"], "starting wealth": "5d4x10", - "armor": ["all"], - "weapons": ["simple", "martial"], + "armor training": ["all"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Rogue", @@ -50,8 +50,8 @@ "primary ability": ["Dexterity"], "saving throws": ["Dexterity", "Intelligence"], "starting wealth": "4d4x10", - "armor": ["light"], - "weapons": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + "armor training": ["light"], + "weapon proficiencies": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], }, { "name": "Wizard", @@ -60,8 +60,8 @@ "primary ability": ["Intelligence"], "saving throws": ["Intelligence", "Wisdom"], "starting wealth": "4d4x10", - "armor": [], - "weapons": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], + "armor training": [], + "weapon proficiencies": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], } ] } diff --git a/RolePlayingCore/RolePlayingCoreTests/TestConfiguration.json b/RolePlayingCore/RolePlayingCoreTests/TestConfiguration.json index b770154..15c1308 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestConfiguration.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestConfiguration.json @@ -1,5 +1,6 @@ { "currencies": ["TestCurrencies"], + "backgrounds": ["TestBackgrounds"], "classes": ["TestClasses", "TestMoreClasses"], "species": ["TestSpecies", "TestMoreSpecies"], "players": ["TestPlayers"] diff --git a/RolePlayingCore/RolePlayingCoreTests/TestMoreClasses.json b/RolePlayingCore/RolePlayingCoreTests/TestMoreClasses.json index 95f2acd..ba7da1f 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestMoreClasses.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestMoreClasses.json @@ -7,8 +7,8 @@ "primary ability": ["Strength"], "saving throws": ["Strength", "Constitution"], "starting wealth": "2d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple", "martial"], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Bard", @@ -17,8 +17,8 @@ "primary ability": ["Charisma"], "saving throws": ["Dexterity", "Charisma"], "starting wealth": "5d4x10", - "armor": ["light"], - "weapons": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + "armor training": ["light"], + "weapon proficiencies": ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], }, { "name": "Druid", @@ -27,8 +27,8 @@ "primary ability": ["Wisdom"], "saving throws": ["Intelligence", "Wisdom"], "starting wealth": "2d4x10", - "armor": ["light", "medium", "shields", "nonmetal"], - "weapons": ["club", "dagger", "dart", "javelin", "mace", "quarterstaff", "scimitar", "sickle", "sling", "spear"], + "armor training": ["light", "medium", "shields", "nonmetal"], + "weapon proficiencies": ["club", "dagger", "dart", "javelin", "mace", "quarterstaff", "scimitar", "sickle", "sling", "spear"], }, { "name": "Monk", @@ -37,8 +37,8 @@ "primary ability": ["Dexterity", "Wisdom"], "saving throws": ["Strength", "Dexterity"], "starting wealth": "5d4", - "armor": [], - "weapons": ["simple", "shortsword"], + "armor training": [], + "weapon proficiencies": ["simple", "shortsword"], }, { "name": "Paladin", @@ -47,8 +47,8 @@ "primary ability": ["Strength", "Charisma"], "saving throws": ["Wisdom", "Charisma"], "starting wealth": "5d4x10", - "armor": ["all"], - "weapons": ["simple", "martial"], + "armor training": ["all"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Ranger", @@ -57,8 +57,8 @@ "primary ability": ["Dexterity", "Wisdom"], "saving throws": ["Strength", "Dexterity"], "starting wealth": "5d4x10", - "armor": ["light", "medium", "shield"], - "weapons": ["simple", "martial"], + "armor training": ["light", "medium", "shield"], + "weapon proficiencies": ["simple", "martial"], }, { "name": "Sorcerer", @@ -67,8 +67,8 @@ "primary ability": ["Charisma"], "saving throws": ["Constitution", "Charisma"], "starting wealth": "3d4x10", - "armor": [], - "weapons": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], + "armor training": [], + "weapon proficiencies": ["dagger", "dart", "sling", "quarterstaff", "light crossbow"], }, { "name": "Warlock", @@ -77,8 +77,8 @@ "primary ability": ["Charisma"], "saving throws": ["Wisdom", "Charisma"], "starting wealth": "4d4x10", - "armor": ["light"], - "weapons": ["simple"], + "armor training": ["light"], + "weapon proficiencies": ["simple"], } ] } diff --git a/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json b/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json index d47eeb7..bf84287 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json @@ -3,7 +3,6 @@ { "name": "Dragonborn", "plural": "Dragonborn", - "ability scores": { "Strength": 2, "Charisma": 1 }, "minimum age": 15, "lifespan": 80, "alignment": "Good", @@ -12,13 +11,12 @@ "base weight": 175, "weight modifier": "2d6", "speed": 30, - "skills": ["draconic ancestry", "breath weapon", "damage"], + "skill proficiencies": ["draconic ancestry", "breath weapon", "damage"], "languages": ["Common", "Draconic"] }, { "name": "Gnome", "plural": "Gnomes", - "ability scores": { "Intelligence": 2 }, "minimum age": 40, "lifespan": 425, "alignment": "Good", @@ -27,25 +25,22 @@ "base weight": 35, "speed": 25, "darkvision": 60, - "skills": ["gnome cunning"], + "skill proficiencies": ["gnome cunning"], "languages": ["Common", "Gnomish"], "subspecies": [{ "name": "Forest Gnome", "plural": "Forest Gnomes", - "ability scores": { "Dexterity": 1 }, - "skills": ["natural illusionist", "speak with small beasts"] + "skill proficiencies": ["natural illusionist", "speak with small beasts"] }, { "name": "Rock Gnome", "plural": "Rock Gnomes", - "ability scores": { "Constitution": 1 }, - "skills": ["artificers lore", "tinker"] + "skill proficiencies": ["artificers lore", "tinker"] }] }, { "name": "Half-Elf", "plural": "Half-Elves", - "ability scores": { "Strength": 1 }, "ability score bonuses": 2, "minimum age": 20, "lifespan": 180, @@ -56,7 +51,7 @@ "weight modifier": "2d4", "speed": 30, "darkvision": 60, - "skills": ["fey ancestry"], + "skill proficiencies": ["fey ancestry"], "languages": ["Common", "Elvish"], "extra languages": 1, "skill versatility": 2 @@ -64,7 +59,6 @@ { "name": "Half-Orc", "plural": "Half-Orcs", - "ability scores": { "Strength": 2, "Constitution": 1 }, "minimum age": 14, "lifespan": 75, "alignment": "Chaotic", @@ -74,13 +68,12 @@ "weight modifier": "2d6", "speed": 30, "darkvision": 60, - "skills": ["menacing", "relentless endurance", "savage attacks"], + "skill proficiencies": ["menacing", "relentless endurance", "savage attacks"], "languages": ["Common", "Orcish"] }, { "name": "Tiefling", "plural": "Tieflings", - "ability scores": { "Intelligence": 1, "Charisma": 2 }, "minimum age": 18, "lifespan": 100, "alignment": "Chaotic", @@ -90,7 +83,7 @@ "weight modifier": "2d4", "speed": 30, "darkvision": 60, - "skills": ["hellish resistance", "infernal legacy"], + "skill proficiencies": ["hellish resistance", "infernal legacy"], "languages": ["Common", "Infernal"] } ] diff --git a/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json b/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json index e84d205..5f9dd79 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json @@ -3,23 +3,29 @@ { "name": "Frodo", "gender": "Male", + "background": "Sailor", "class": "Cleric", "species": "High Elf", "height": "3'9\"", "weight": 120, "ability scores": {"Dexterity": 13}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skills": ["Athletics"], "money": 130, "maximum hit points": 10 }, { "name": "Bilbo", "gender": "Female", + "background": "Sailor", "class": "Fighter", "species": "Human", "alignment": "Lawful Evil", "height": "3'9\"", "weight": 120, "ability scores": {"Strength": 12}, + "background ability scores": ["Strength", "Dexterity", "Dexterity"], + "skills": ["Athletics"], "money": 130, "maximum hit points": 10, "experience points": 2300, diff --git a/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json b/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json index ce7e696..9ff4b47 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json @@ -3,7 +3,6 @@ { "name": "Dwarf", "plural": "Dwarves", - "ability scores": { "Constitution": 2 }, "minimum age": 50, "lifespan": 350, "alignment": "Lawful Good", @@ -13,15 +12,14 @@ "weight modifier": "2d6", "speed": 25, "darkvision": 60, - "skills": ["dwarven resilience"], - "weapons" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], + "skill proficiencies": ["dwarven resilience"], + "weapon proficiencies" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], "tools": ["smith's tools", "brewer's supplies", "mason's tools"], "history": ["stonecunning"], "languages": ["Common", "Dwarvish"], "subspecies": [{ "name": "Hill Dwarf", "plural": "Hill Dwarves", - "ability scores": { "Wisdom": 2 }, "hit point bonus": 1, "base height": "3'5\"", "base weight": 115, @@ -29,14 +27,12 @@ { "name": "Mountain Dwarf", "plural": "Mountain Dwarves", - "ability scores": { "Strength": 2 }, - "armor": ["light", "medium"], + "armor training": ["light", "medium"], }] }, { "name": "Elf", "plural": "Elves", - "ability scores": { "Dexterity": 2 }, "minimum age": 100, "lifespan": 750, "alignment": "Chaotic Good", @@ -47,14 +43,13 @@ "speed": 30, "darkvision": 60, "resilience": ["poison", "poison damage"], - "skills": ["keen senses", "fey ancestry", "trance"], + "skill proficiencies": ["keen senses", "fey ancestry", "trance"], "languages": ["Common", "Elvish"], "subspecies": [{ "name": "High Elf", "plural": "High Elves", - "ability scores": { "Intelligence": 1 }, "base weight:": 90, - "weapons": ["longsword", "shortsword", "shortbow", "longbow"], + "weapon proficiencies": ["longsword", "shortsword", "shortbow", "longbow"], "extra languages": 1, "spells": ["wizard cantrip"], "hit point bonus": 1 @@ -62,28 +57,25 @@ { "name": "Wood Elf", "plural": "Wood Elves", - "ability scores": { "Wisdom": 1 }, - "weapons": ["longsword", "shortsword", "shortbow", "longbow"], - "skills": ["fleet of foot", "mask of the wild"] + "weapon proficiencies": ["longsword", "shortsword", "shortbow", "longbow"], + "skill proficiencies": ["fleet of foot", "mask of the wild"] }, { "name": "Dark Elf", "plural": "Dark Elves", "aliases": ["Drow"], - "ability scores": { "Charisma": 1 }, "base height": "4'5\"", "height modifier": "2d6", "base weight": 75, "weight modifier": "1d6", "darkvision": 120, - "weapons": ["rapier", "shortsword", "hand crossbow"], + "weapon proficiencies": ["rapier", "shortsword", "hand crossbow"], "spells": ["dancing lights", "faerie fire:3", "darkness:5"] }] }, { "name": "Halfling", "plural": "Halflings", - "ability scores": { "Dexterity": 2 }, "minimum age": 20, "lifespan": 150, "alignment": "Lawful Good", @@ -91,28 +83,25 @@ "base weight": 35, "height modifier": "2d4", "speed": 25, - "skills": ["lucky", "brave", "halfling nimbleness"], - "weapons" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], + "skill proficiencies": ["lucky", "brave", "halfling nimbleness"], + "weapon proficiencies" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], "tools": ["smith's tools", "brewer's supplies", "mason's tools"], "history": ["stonecunning"], "languages": ["Common", "Halfling"], "subspecies": [{ "name": "Lightfoot", "plural": "Lightfoots", - "ability scores": { "Charisma": 1 }, - "skills": ["stealthy"] + "skill proficiencies": ["stealthy"] }, { "name": "Stout", "plural": "Stouts", - "ability scores": { "Constitution": 1 }, - "skills": ["stout resilience"] + "skill proficiencies": ["stout resilience"] }] }, { "name": "Human", "plural": "Humans", - "ability scores": { "Strength": 1, "Dexterity": 1, "Constitution": 1, "Intelligence": 1, "Wisdom": 1, "Charisma": 1 }, "minimum age": 18, "lifespan": 90, "base height": "4'8\"", From 3deec5b835bd2fdb82709c2d66ac7e006065b4b9 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Tue, 28 Oct 2025 07:17:03 -0400 Subject: [PATCH 02/33] Updates to SpeciesTraits. --- .../Configuration/Species.json | 318 +++++++----------- .../Configuration/SpeciesNames.json | 19 +- .../Player/CharacterSheet.swift | 11 +- .../RolePlayingCore.xcodeproj/project.pbxproj | 4 + .../Configuration/CharacterGenerator.swift | 2 +- .../RolePlayingCore/Player/CreatureSize.swift | 87 +++++ .../RolePlayingCore/Player/Player.swift | 19 +- .../Player/SpeciesTraits.swift | 182 +++------- .../InvalidClassPlayers.json | 1 - .../InvalidSpeciesPlayers.json | 1 - .../MissingClassPlayers.json | 1 - .../MissingSpeciesPlayers.json | 1 - .../RolePlayingCoreTests/PlayerTests.swift | 18 +- .../SpeciesTraitsTests.swift | 146 +------- .../RolePlayingCoreTests/TestMoreSpecies.json | 29 -- .../RolePlayingCoreTests/TestPlayers.json | 2 - .../RolePlayingCoreTests/TestSpecies.json | 175 ++++------ 17 files changed, 349 insertions(+), 667 deletions(-) create mode 100644 RolePlayingCore/RolePlayingCore/Player/CreatureSize.swift diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Species.json b/CharacterGenerator/CharacterGenerator/Configuration/Species.json index 0f29c13..68e61f8 100644 --- a/CharacterGenerator/CharacterGenerator/Configuration/Species.json +++ b/CharacterGenerator/CharacterGenerator/Configuration/Species.json @@ -1,202 +1,122 @@ { "species": [ - { - "name": "Dwarf", - "plural": "Dwarves", - "minimum age": 50, - "lifespan": 350, - "alignment": "Lawful Good", - "base height": 4, - "height modifier": "2d4", - "base weight": 130, - "weight modifier": "2d6", - "speed": 25, - "darkvision": 60, - "skill proficiencies": ["dwarven resilience"], - "weapon proficiencies" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], - "tools": ["smith's tools", "brewer's supplies", "mason's tools"], - "history": ["stonecunning"], - "languages": ["Common", "Dwarvish"], - "subspecies": [{ - "name": "Hill Dwarf", - "plural": "Hill Dwarves", - "hit point bonus": 1, - "base height": "3'5\"", - "base weight": 115, - }, - { - "name": "Mountain Dwarf", - "plural": "Mountain Dwarves", - "armor": ["light", "medium"], - }] - }, - { - "name": "Elf", - "plural": "Elves", - "minimum age": 100, - "lifespan": 750, - "alignment": "Chaotic Good", - "base height": "4'6\"", - "height modifier": "2d10", - "base weight": 100, - "weight modifier": "1d4", - "speed": 30, - "darkvision": 60, - "resilience": ["poison", "poison damage"], - "skill proficiencies": ["keen senses", "fey ancestry", "trance"], - "languages": ["Common", "Elvish"], - "subspecies": [{ - "name": "High Elf", - "plural": "High Elves", - "base weight:": 90, - "weapon proficiencies": ["longsword", "shortsword", "shortbow", "longbow"], - "extra languages": 1, - "spells": ["wizard cantrip"], - "hit point bonus": 1 - }, - { - "name": "Wood Elf", - "plural": "Wood Elves", - "weapon proficiencies": ["longsword", "shortsword", "shortbow", "longbow"], - "skill proficiencies": ["fleet of foot", "mask of the wild"] - }, - { - "name": "Dark Elf", - "plural": "Dark Elves", - "aliases": ["Drow"], - "base height": "4'5\"", - "height modifier": "2d6", - "base weight": 75, - "weight modifier": "1d6", - "darkvision": 120, - "weapon proficiencies": ["rapier", "shortsword", "hand crossbow"], - "spells": ["dancing lights", "faerie fire:3", "darkness:5"] - }] - }, - { - "name": "Halfling", - "plural": "Halflings", - "minimum age": 20, - "lifespan": 150, - "alignment": "Lawful Good", - "base height": "2'7\"", - "base weight": 35, - "height modifier": "2d4", - "speed": 25, - "skill proficiencies": ["lucky", "brave", "halfling nimbleness"], - "weapon proficiencies" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], - "tools": ["smith's tools", "brewer's supplies", "mason's tools"], - "history": ["stonecunning"], - "languages": ["Common", "Halfling"], - "subspecies": [{ - "name": "Lightfoot", - "plural": "Lightfoots", - "skill proficiencies": ["stealthy"] - }, - { - "name": "Stout", - "plural": "Stouts", - "skill proficiencies": ["stout resilience"] - }] - }, - { - "name": "Human", - "plural": "Humans", - "minimum age": 18, - "lifespan": 90, - "base height": "4'8\"", - "height modifier": "2d10", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "languages": ["Common"], - "extra languages": 1 - }, - { - "name": "Dragonborn", - "plural": "Dragonborn", - "minimum age": 15, - "lifespan": 80, - "alignment": "Good", - "base height": "5'6\"", - "height modifier": "2d8", - "base weight": 175, - "weight modifier": "2d6", - "speed": 30, - "skill proficiencies": ["draconic ancestry", "breath weapon", "damage"], - "languages": ["Common", "Draconic"] - }, - { - "name": "Gnome", - "plural": "Gnomes", - "minimum age": 40, - "lifespan": 425, - "alignment": "Good", - "base height": "2'11\"", - "height modifier": "2d4", - "base weight": 35, - "speed": 25, - "darkvision": 60, - "skill proficiencies": ["gnome cunning"], - "languages": ["Common", "Gnomish"], - "subspecies": [{ - "name": "Forest Gnome", - "plural": "Forest Gnomes", - "skill proficiencies": ["natural illusionist", "speak with small beasts"] - }, - { - "name": "Rock Gnome", - "plural": "Rock Gnomes", - "skill proficiencies": ["artificers lore", "tinker"] - }] - }, - { - "name": "Half-Elf", - "plural": "Half-Elves", - "ability score bonuses": 2, - "minimum age": 20, - "lifespan": 180, - "alignment": "Chaotic", - "base height": "4'9\"", - "height modifier": "2d8", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "darkvision": 60, - "skill proficiencies": ["fey ancestry"], - "languages": ["Common", "Elvish"], - "extra languages": 1, - "skill versatility": 2 - }, - { - "name": "Half-Orc", - "plural": "Half-Orcs", - "minimum age": 14, - "lifespan": 75, - "alignment": "Chaotic", - "base height": "4'10\"", - "height modifier": "2d10", - "base weight": 140, - "weight modifier": "2d6", - "speed": 30, - "darkvision": 60, - "skill proficiencies": ["menacing", "relentless endurance", "savage attacks"], - "languages": ["Common", "Orcish"] - }, - { - "name": "Tiefling", - "plural": "Tieflings", - "minimum age": 18, - "lifespan": 100, - "alignment": "Chaotic", - "base height": "4'9\"", - "height modifier": "2d8", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "darkvision": 60, - "skill proficiencies": ["hellish resistance", "infernal legacy"], - "languages": ["Common", "Infernal"] - } - ] + { + "name": "Aasimar", + "plural": "Aasimar", + "lifespan": 160, + "base sizes": ["4-7 ft", "2-4 ft"], + "speed": 30, + "darkvision": 60 + }, + { + "name": "Dragonborn", + "plural": "Dragonborn", + "lifespan": 80, + "base sizes": ["5-7 ft"], + "speed": 30, + "darkvision": 60, + }, + { + "name": "Dwarf", + "plural": "Dwarves", + "lifespan": 350, + "base sizes": ["4-5 ft"], + "speed": 30, + "darkvision": 120, + "subspecies": [{ + "name": "Hill Dwarf", + "plural": "Hill Dwarves", + }, + { + "name": "Mountain Dwarf", + "plural": "Mountain Dwarves", + }] + }, + { + "name": "Elf", + "plural": "Elves", + "lifespan": 750, + "base sizes": ["5-6 ft"], + "speed": 30, + "darkvision": 60, + "subspecies": [{ + "name": "High Elf", + "plural": "High Elves" + }, + { + "name": "Wood Elf", + "plural": "Wood Elves" + }, + { + "name": "Drow", + "plural": "Drow", + "aliases": ["Dark Elf"], + "darkvision": 120 + }] + }, + { + "name": "Gnome", + "plural": "Gnomes", + "lifespan": 425, + "base sizes": ["3-4 ft"], + "speed": 30, + "darkvision": 60, + "subspecies": [{ + "name": "Forest Gnome", + "plural": "Forest Gnomes", + }, + { + "name": "Rock Gnome", + "plural": "Rock Gnomes", + }] + }, + { + "name": "Goliath", + "plural": "Goliaths", + "lifespan": 80, + "base sizes": ["7-8 ft"], + "speed": 35 + }, + { + "name": "Halfling", + "plural": "Halflings", + "lifespan": 150, + "base sizes": ["2-3 ft"], + "speed": 30, + "subspecies": [{ + "name": "Lightfoot", + "plural": "Lightfoots", + "aliases": ["Tallfellow"] + }, + { + "name": "Stout", + "plural": "Stouts", + "aliases": ["Strongheart"] + }] + + }, + { + "name": "Human", + "plural": "Humans", + "lifespan": 80, + "base sizes": ["4-7 ft", "2-4 ft"], + "speed": 30 + }, + { + "name": "Orc", + "plural": "Orcs", + "lifespan": 80, + "base sizes": ["6-7 ft"], + "speed": 30, + "darkvision": 120 + }, + { + "name": "Tiefling", + "plural": "Tieflings", + "lifespan": 80, + "base sizes": ["4-7 ft", "3-4 ft"], + "speed": 30, + "darkvision": 60 + } + ] } diff --git a/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json b/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json index 664fd79..d4816c1 100644 --- a/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json +++ b/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json @@ -1,5 +1,11 @@ { "names": { + "Aasimar": { + "family type": "Family", + "Male": ["Aliban", "Mykiel", "Tadriel", "Valandras", "Diero", "Evendur", "Hunin", "Taman", "Zasheir", "Azrael", "Castiel", "Metatron", "Remiel", "Sachiel"], + "Female": ["Galladia", "Myllandra", "Seraphina", "Arveene", "Nephis", "Reani", "Selise", "Yasha", "Zora", "Jhessail", "Ananiel", "Jophiel", "Sariel", "Zuriel"], + "family": ["Amolune", "Solvaar", "Vaerthine", "Brightsun", "Moontide", "Whisperwind"] + }, "Dwarf": { "family type": "Clan", "Male": ["Adrik", "Alberich", "Baern", "Barendd", "Brottor", "Flint", "Dain", "Darrak", "Delg", "Eberk", "Einkil", "Fargrim", "Flint", "Gardain", "Harbek", "Kildrak", "Morgran", "Orsik", "Oskar", "Rangrim", "Rurik", "Talkinn", "Thoradin", "Thorin", "Tordek", "Traubon", "Travok", "Ulfgar", "Veit", "Vondal"], @@ -33,6 +39,13 @@ "family": ["Clethtinthiallor", "Daardendrian", "Delmirev", "Drachedandion", "Fenkenkabradon", "Kepeshkmolik", "Kerrhylon", "Kimbatuul", "Linxakasendalor", "Myastan", "Nemmonis", "Norixius", "Ophinshtalajiir", "Prexijandilin", "Shestendeliath", "Turnuroth", "Verthisathurgiesh", "Yarjerit"], "child": ["Climber", "Earbender", "Leaper", "Pious", "Shieldbiter", "Zealous"] }, + "Goliath": { + "family type": "Clan", + "Male": ["Aukan", "Eglath", "Gae-Al", "Gauthak", "Ilikan", "Keothi", "Kuori", "Lo-Kag", "Manneo", "Maveith", "Nalla", "Orilo", "Paavu", "Pethani", "Thalai", "Thotham", "Uthal", "Vaunea", "Vimak"], + "Female": ["Aukan", "Eglath", "Gae-Al", "Gauthak", "Ilikan", "Keothi", "Kuori", "Lo-Kag", "Manneo", "Maveith", "Nalla", "Orilo", "Paavu", "Pethani", "Thalai", "Thotham", "Uthal", "Vaunea", "Vimak"], + "family": ["Anakalathai", "Elanithino", "Gathakanathi", "Kalagiano", "Katho-Olavi", "Kolae-Gileana", "Ogolakanu", "Thuliaga", "Thunukalathi", "Vaimei-Laga"], + "nickname": ["Bearkiller", "Dawncaller", "Fearless", "Flintfinder", "Horncarver", "Keeneye", "Lonehunter", "Longleaper", "Rootsmasher", "Skywatcher", "Steadyhand", "Threadtwister", "Twice-Orphaned", "Twistedlimb", "Wordpainter"] + }, "Gnome": { "family type": "Clan", "Male": ["Alston", "Alvyn", "Boddynock", "Brocc", "Burgell", "Dimble", "Eldon", "Erky", "Fonkin", "Frug", "Gerbo", "Gimble", "Glim", "Jebeddo", "Kellen", "Namfoodle", "Orryn", "Roondar", "Seebo", "Sindri", "Warryn", "Wrenn", "Zook"], @@ -40,12 +53,6 @@ "family": ["Beren", "Daergel", "Folkor", "Garrick", "Nackle", "Murning", "Ningel", "Raulnor", "Scheppen", "Timbers", "Turen"], "nickname": ["Aleslosh", "Ashhearth", "Badger", "Cloak", "Doublelock", "Filchbatter", "Fnipper", "Ku", "Nim", "Oneshoe", "Pock", "Sparklegem", "Stumbleduck"] }, - "Half-Elf": { - "aliases": ["Elf", "Human"] - }, - "Half-Orc": { - "aliases": ["Orc", "Human"] - }, "Orc": { "family type": "", "Male": ["Dench", "Feng", "Gell", "Henk", "Holg", "Imsh", "Keth", "Krusk", "Mhurren", "Ront", "Shump", "Thokk"], diff --git a/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift b/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift index 3ad55ec..7a37a05 100644 --- a/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift +++ b/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift @@ -29,10 +29,10 @@ class CharacterSheet { [\.backgroundName, \.speciesName, \.className], [\.abilities], [\.skills], - [\.initiative, \.speed, \.size], + [\.initiative, \.speed], [\.armorClass, \.proficiencyBonus, \.passivePerception], [\.maximumHitPoints, \.hitDice], - [\.height, \.weight], + [\.height, \.size], [\.money] ] @@ -42,10 +42,10 @@ class CharacterSheet { ["Background", "Species", "Class", "Subclass"], ["Abilities"], ["Skills"], - ["Initiative", "Speed", "Size"], + ["Initiative", "Speed"], ["Armor Class", "Proficiency Bonus", "Passive Perception"], ["Hit Points", "Hit Dice"], - ["Height", "Weight"], + ["Height", "Size"], ["Money"] ] @@ -55,7 +55,7 @@ class CharacterSheet { ["labeledText", "labeledText", "labeledText"], ["abilities"], ["labeledText"], - ["labeledNumber", "labeledNumber", "labeledText"], + ["labeledNumber", "labeledNumber"], ["labeledNumber", "labeledNumber", "labeledNumber"], ["labeledNumber", "labeledText"], ["labeledText", "labeledText"], @@ -95,7 +95,6 @@ class CharacterSheet { var money: String { "\(player.money)" } var gender: String { player.gender.map(\.rawValue) ?? "Androgynous" } var height: String { player.height.displayString } - var weight: String { player.weight.displayString } var speed: String { let value = player.speed let distance = Measurement(value: Double(value), unit: UnitLength.feet) diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj index 3b3cb29..c4437c8 100644 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj +++ b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ B69F84751E591E9800A4D2B0 /* InvalidSpeciesPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */; }; B6A1AE591F0E4C59008ADF08 /* InvalidConfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE581F0E4C59008ADF08 /* InvalidConfiguration.json */; }; B6A29EB51EFE9B9F00DAB40C /* Currencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A29EB41EFE9B9F00DAB40C /* Currencies.swift */; }; + B6C3076E2EAFBEC10066D9F0 /* CreatureSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */; }; B6CF53901E51DA1300CADD9F /* ClassTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */; }; B6CF53921E51DEDD00CADD9F /* ClassTraitsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */; }; B6CF53CE1E54DF4500CADD9F /* JSONFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53CD1E54DF4500CADD9F /* JSONFile.swift */; }; @@ -141,6 +142,7 @@ B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidSpeciesPlayers.json; sourceTree = ""; }; B6A1AE581F0E4C59008ADF08 /* InvalidConfiguration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = InvalidConfiguration.json; sourceTree = ""; }; B6A29EB41EFE9B9F00DAB40C /* Currencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Currencies.swift; sourceTree = ""; }; + B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatureSize.swift; sourceTree = ""; }; B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassTraits.swift; sourceTree = ""; }; B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassTraitsTests.swift; sourceTree = ""; }; B6CF53CD1E54DF4500CADD9F /* JSONFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFile.swift; sourceTree = ""; }; @@ -334,6 +336,7 @@ B6688B502EACF5AE000A83DD /* Initiative.swift */, B69F846C1E58D66900A4D2B0 /* Players.swift */, B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */, + B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */, B6CF53D11E54E2EF00CADD9F /* Species.swift */, ); path = Player; @@ -485,6 +488,7 @@ B6A29EB51EFE9B9F00DAB40C /* Currencies.swift in Sources */, B66AF9051EAE88C300C15F8E /* Configuration.swift in Sources */, B6CF53CE1E54DF4500CADD9F /* JSONFile.swift in Sources */, + B6C3076E2EAFBEC10066D9F0 /* CreatureSize.swift in Sources */, B6CF53D21E54E2EF00CADD9F /* Species.swift in Sources */, B6FA6CB01E47ACA1004D91B1 /* UnitCurrency.swift in Sources */, B6CF53901E51DA1300CADD9F /* ClassTraits.swift in Sources */, diff --git a/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift b/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift index 81eba2f..5aa2df8 100644 --- a/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift +++ b/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift @@ -45,7 +45,7 @@ public struct CharacterGenerator { let speciesTraits = configuration.species.randomElementByIndex(using: &generator) let classTraits = configuration.classes[randomClass]! let name = names.randomName(speciesTraits: speciesTraits, gender: gender, using: &generator) - let alignment = speciesTraits.alignment != nil ? speciesTraits.alignment : randomAlignment(using: &generator) + let alignment = randomAlignment(using: &generator) return Player(name, backgroundTraits: backgroundTraits, speciesTraits: speciesTraits, classTraits: classTraits, gender: gender, alignment: alignment) } diff --git a/RolePlayingCore/RolePlayingCore/Player/CreatureSize.swift b/RolePlayingCore/RolePlayingCore/Player/CreatureSize.swift new file mode 100644 index 0000000..c495147 --- /dev/null +++ b/RolePlayingCore/RolePlayingCore/Player/CreatureSize.swift @@ -0,0 +1,87 @@ +// +// CreatureSize.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/27/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +public enum CreatureSize: String { + case tiny + case small + case medium + case large + case huge + case gargantuan + + init(from height: Height) { + let heightInFeet = height.converted(to: .feet) + switch heightInFeet.value { + case 0..<4: + self = .small + case 4..<8: + self = .medium + default: + self = .large + } + } + + // Integer range in inches + var range: Range { + switch self { + case .tiny: return 12..<24 + case .small: return 24..<48 + case .medium: return 48..<96 + case .large: return 96..<120 + case .huge: return 120..<180 + case .gargantuan: return 180..<240 + } + } + + /// Space required in feet (dimension feet x feet) + var space: Double { + switch self { + case .tiny: return 2.5 + case .small, .medium: return 5.0 + case .large: return 10.0 + case .huge: return 15.0 + case .gargantuan: return 20.0 + } + } + + /// Space required in squares + var squares: Double { + switch self { + case .tiny: return 0.25 + case .small, .medium: return 1.0 + case .large: return 4.0 + case .huge: return 9.0 + case .gargantuan: return 16.0 + } + } +} + +extension Height { + + /// Generates a random height from a named size, or a string range "min-max" appended with "ft" (feet) or "in" (inches). + public static func randomHeight(from rangeString: String) -> Height { + let range: Range + if let namedSize = CreatureSize(rawValue: rangeString) { + range = namedSize.range + } else if rangeString.hasSuffix("ft") { + let minMaxString = rangeString.replacing("ft", with: "").replacing(" ", with: "").components(separatedBy: "-") + let minValue = (Int(minMaxString[0]) ?? 4) * 12 + let maxValue = (Int(minMaxString[1]) ?? 7) * 12 + range = minValue.. Int { - return max(classTraits.hitDice.sides / 2 + 1, classTraits.hitDice.roll().result) + speciesTraits.hitPointBonus + return max(classTraits.hitDice.sides / 2 + 1, classTraits.hitDice.roll().result) } public func rollHitPoints() -> Int { @@ -277,7 +270,6 @@ extension Player: Hashable { lhs.gender == rhs.gender && lhs.alignment == rhs.alignment && lhs.height == rhs.height && - lhs.weight == rhs.weight && lhs.baseAbilities == rhs.baseAbilities && lhs.maximumHitPoints == rhs.maximumHitPoints && lhs.currentHitPoints == rhs.currentHitPoints && @@ -294,7 +286,6 @@ extension Player: Hashable { hasher.combine(gender) hasher.combine(alignment) hasher.combine(height) - hasher.combine(weight) hasher.combine(baseAbilities) hasher.combine(maximumHitPoints) hasher.combine(currentHitPoints) diff --git a/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift b/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift index bf6bb18..1f1ac45 100644 --- a/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift +++ b/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift @@ -10,97 +10,58 @@ import Foundation public struct SpeciesTraits { + public struct CreatureType { + public let name: String + + public static let aberration = CreatureType(name: "Aberration") + public static let beast = CreatureType(name: "Beast") + public static let celestial = CreatureType(name: "Celestial") + public static let construct = CreatureType(name: "Construct") + public static let dragon = CreatureType(name: "Dragon") + public static let elemental = CreatureType(name: "Elemental") + public static let fey = CreatureType(name: "Fey") + public static let fiend = CreatureType(name: "Fiend") + public static let giant = CreatureType(name: "Giant") + public static let humanoid = CreatureType(name: "Humanoid") + public static let monstrosity = CreatureType(name: "Monstrosity") + public static let ooze = CreatureType(name: "Ooze") + public static let plant = CreatureType(name: "Plant") + public static let undead = CreatureType(name: "Undead") + } public var name: String public var plural: String public var aliases: [String] + public var creatureType: CreatureType public var descriptiveTraits: [String: String] - public var minimumAge: Int! public var lifespan: Int! - public var alignment: Alignment? - public var baseHeight: Height! - public var heightModifier: Dice - public var baseWeight: Weight! - public var weightModifier: Dice + + public var baseSizes: [String] + public var darkVision: Int! public var speed: Int! - public var hitPointBonus: Int public var parentName: String? public var subspecies: [SpeciesTraits] = [] - public enum Size { - case tiny - case small - case medium - case large - case huge - case gargantuan - - init(from height: Height) { - let heightInFeet = height.converted(to: .feet) - switch heightInFeet.value { - case 0..<4: - self = .small - case 4..<7: - self = .medium - default: - self = .large - } - } - - /// Space required in feet (dimension feet x feet) - var space: Double { - switch self { - case .tiny: return 2.5 - case .small, .medium: return 5.0 - case .large: return 10.0 - case .huge: return 15.0 - case .gargantuan: return 20.0 - } - } - - /// Space required in squares - var squares: Double { - switch self { - case .tiny: return 0.25 - case .small, .medium: return 1.0 - case .large: return 4.0 - case .huge: return 9.0 - case .gargantuan: return 16.0 - } - } - } - - public var size: Size { Size(from: baseHeight) } - public init(name: String, plural: String, aliases: [String] = [], + creatureType: CreatureType = .humanoid, descriptiveTraits: [String: String] = [:], - minimumAge: Int, lifespan: Int, - alignment: Alignment? = nil, - baseHeight: Height, - heightModifier: Dice = DiceModifier(0), - baseWeight: Weight, - weightModifier: Dice = DiceModifier(0), + baseSizes: [String] = ["4-7"], darkVision: Int, speed: Int, hitPointBonus: Int = 0) { self.name = name self.plural = plural self.aliases = aliases + self.creatureType = creatureType self.descriptiveTraits = descriptiveTraits - self.minimumAge = minimumAge self.lifespan = lifespan - self.alignment = alignment - self.baseHeight = baseHeight - self.heightModifier = heightModifier - self.baseWeight = baseWeight - self.weightModifier = weightModifier + self.baseSizes = baseSizes self.darkVision = darkVision self.speed = speed - self.hitPointBonus = hitPointBonus } } @@ -110,17 +71,12 @@ extension SpeciesTraits: Codable { case name case plural case aliases + case creatureType = "creature type" case descriptiveTraits = "descriptive traits" - case minimumAge = "minimum age" case lifespan - case alignment - case baseHeight = "base height" - case heightModifier = "height modifier" - case baseWeight = "base weight" - case weightModifier = "weight modifier" + case baseSizes = "base sizes" case darkVision = "darkvision" case speed - case hitPointBonus = "hit point bonus" case subspecies } @@ -131,33 +87,23 @@ extension SpeciesTraits: Codable { let name = try values.decode(String.self, forKey: .name) let plural = try values.decode(String.self, forKey: .plural) let aliases = try values.decodeIfPresent([String].self, forKey: .aliases) + let creatureType = try values.decodeIfPresent(String.self, forKey: .creatureType) let descriptiveTraits = try values.decodeIfPresent([String:String].self, forKey: .descriptiveTraits) - let minimumAge = try values.decodeIfPresent(Int.self, forKey: .minimumAge) let lifespan = try values.decodeIfPresent(Int.self, forKey: .lifespan) - let alignment = try values.decodeIfPresent(Alignment.self, forKey: .alignment) - let baseHeight = try values.decodeIfPresent(Height.self, forKey: .baseHeight) - let heightModifier = try values.decodeIfPresent(Dice.self, forKey: .heightModifier) - let baseWeight = try values.decodeIfPresent(Weight.self, forKey: .baseWeight) - let weightModifier = try values.decodeIfPresent(Dice.self, forKey: .weightModifier) + let baseSizes = try values.decodeIfPresent([String].self, forKey: .baseSizes) let darkVision = try values.decodeIfPresent(Int.self, forKey: .darkVision) let speed = try values.decodeIfPresent(Int.self, forKey: .speed) - let hitPointBonus = try values.decodeIfPresent(Int.self, forKey: .hitPointBonus) // Safely set properties self.name = name self.plural = plural self.aliases = aliases ?? [] + self.creatureType = CreatureType(name: creatureType ?? CreatureType.humanoid.name) self.descriptiveTraits = descriptiveTraits ?? [:] - self.minimumAge = minimumAge self.lifespan = lifespan - self.alignment = alignment - self.baseHeight = baseHeight - self.heightModifier = heightModifier ?? DiceModifier(0) - self.baseWeight = baseWeight - self.weightModifier = weightModifier ?? DiceModifier(0) + self.baseSizes = baseSizes ?? ["4-7"] self.darkVision = darkVision self.speed = speed - self.hitPointBonus = hitPointBonus ?? 0 // Decode subspecies if var subspecies = try? values.nestedUnkeyedContainer(forKey: .subspecies) { @@ -167,7 +113,6 @@ extension SpeciesTraits: Codable { self.subspecies.append(subspeciesTraits) } } - } /// Inherit parent traits, for each trait that is not already set. @@ -175,37 +120,22 @@ extension SpeciesTraits: Codable { // Name, plural, aliases and descriptive traits are unique to each set of species traits. // The rest may be inherited from the parent. self.parentName = parent.name + self.creatureType = parent.creatureType - if self.minimumAge == nil { - self.minimumAge = parent.minimumAge + if self.baseSizes.isEmpty { + self.baseSizes = parent.baseSizes } + if self.lifespan == nil { self.lifespan = parent.lifespan } - if self.alignment == nil { - self.alignment = parent.alignment - } - if self.baseHeight == nil { - self.baseHeight = parent.baseHeight - } - if self.heightModifier.sides == 0 { - self.heightModifier = parent.heightModifier - } - if self.baseWeight == nil { - self.baseWeight = parent.baseWeight - } - if self.weightModifier.sides == 0 { - self.weightModifier = parent.weightModifier - } + if self.darkVision == nil { self.darkVision = parent.darkVision } if self.speed == nil { self.speed = parent.speed } - if self.hitPointBonus == 0 { - self.hitPointBonus = parent.hitPointBonus - } } public func encode(to encoder: Encoder) throws { @@ -214,22 +144,12 @@ extension SpeciesTraits: Codable { try values.encode(name, forKey: .name) try values.encode(plural, forKey: .plural) try values.encode(aliases, forKey: .aliases) + try values.encode(creatureType.name, forKey: .creatureType) try values.encode(descriptiveTraits, forKey: .descriptiveTraits) - try values.encode(minimumAge, forKey: .minimumAge) try values.encode(lifespan, forKey: .lifespan) - - // Encode alignment using its enum string values, since SpeciesTraits is a type. - if alignment != nil { - try values.encode("\(alignment!)", forKey: .alignment) - } - - try values.encode("\(baseHeight!)", forKey: .baseHeight) - try values.encode("\(heightModifier)", forKey: .heightModifier) - try values.encode("\(baseWeight!)", forKey: .baseWeight) - try values.encode("\(weightModifier)", forKey: .weightModifier) + try values.encode(baseSizes, forKey: .baseSizes) try values.encode(darkVision, forKey: .darkVision) try values.encode(speed, forKey: .speed) - try values.encode(hitPointBonus, forKey: .hitPointBonus) var subspeciesContainer = values.nestedUnkeyedContainer(forKey: .subspecies) for subspeciesTraits in subspecies { @@ -244,33 +164,18 @@ extension SpeciesTraits: Codable { try values.encode(name, forKey: .name) try values.encode(plural, forKey: .plural) + try values.encode(creatureType.name, forKey: .creatureType) if self.aliases.count > 0 { try values.encode(aliases, forKey: .aliases) } if self.descriptiveTraits.count > 0 { try values.encode(descriptiveTraits, forKey: .descriptiveTraits) } - - if self.minimumAge != parent.minimumAge { - try values.encode(self.minimumAge, forKey: .minimumAge) - } if self.lifespan != parent.lifespan { try values.encode(self.lifespan, forKey: .lifespan) } - if self.alignment != nil && self.alignment != parent.alignment { - try values.encode("\(alignment!)", forKey: .alignment) - } - if self.baseHeight != parent.baseHeight { - try values.encode("\(self.baseHeight!)", forKey: .baseHeight) - } - if self.heightModifier.sides != parent.heightModifier.sides { - try values.encode("\(self.heightModifier)", forKey: .heightModifier) - } - if self.baseWeight != parent.baseWeight { - try values.encode("\(self.baseWeight!)", forKey: .baseWeight) - } - if self.weightModifier.sides != parent.weightModifier.sides { - try values.encode("\(self.weightModifier)", forKey: .weightModifier) + if self.baseSizes != parent.baseSizes { + try values.encode(self.baseSizes, forKey: .baseSizes) } if self.darkVision != parent.darkVision { try values.encode(self.darkVision, forKey: .darkVision) @@ -278,9 +183,6 @@ extension SpeciesTraits: Codable { if self.speed != parent.speed { try values.encode(self.speed, forKey: .speed) } - if self.hitPointBonus != parent.hitPointBonus { - try values.encode(self.hitPointBonus, forKey: .hitPointBonus) - } } } diff --git a/RolePlayingCore/RolePlayingCoreTests/InvalidClassPlayers.json b/RolePlayingCore/RolePlayingCoreTests/InvalidClassPlayers.json index 5cc545a..5bff776 100644 --- a/RolePlayingCore/RolePlayingCoreTests/InvalidClassPlayers.json +++ b/RolePlayingCore/RolePlayingCoreTests/InvalidClassPlayers.json @@ -6,7 +6,6 @@ "class": "Software Engineer", "species": "High Elf", "height": "3'9\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "money": 130, "maximum hit points": 10 diff --git a/RolePlayingCore/RolePlayingCoreTests/InvalidSpeciesPlayers.json b/RolePlayingCore/RolePlayingCoreTests/InvalidSpeciesPlayers.json index 8303c2a..897cdd3 100644 --- a/RolePlayingCore/RolePlayingCoreTests/InvalidSpeciesPlayers.json +++ b/RolePlayingCore/RolePlayingCoreTests/InvalidSpeciesPlayers.json @@ -7,7 +7,6 @@ "species": "Frog", "alignment": "Lawful Evil", "height": "3'9\"", - "weight": 120, "ability scores": {"Strength": 12}, "money": 130, "maximum hit points": 10, diff --git a/RolePlayingCore/RolePlayingCoreTests/MissingClassPlayers.json b/RolePlayingCore/RolePlayingCoreTests/MissingClassPlayers.json index 7fddeeb..97a0881 100644 --- a/RolePlayingCore/RolePlayingCoreTests/MissingClassPlayers.json +++ b/RolePlayingCore/RolePlayingCoreTests/MissingClassPlayers.json @@ -5,7 +5,6 @@ "gender": "Male", "species": "High Elf", "height": "3'9\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "money": 130, "maximum hit points": 10 diff --git a/RolePlayingCore/RolePlayingCoreTests/MissingSpeciesPlayers.json b/RolePlayingCore/RolePlayingCoreTests/MissingSpeciesPlayers.json index 4a975f0..3f77de7 100644 --- a/RolePlayingCore/RolePlayingCoreTests/MissingSpeciesPlayers.json +++ b/RolePlayingCore/RolePlayingCoreTests/MissingSpeciesPlayers.json @@ -5,7 +5,6 @@ "gender": "Male", "class": "Fighter", "height": "3'9\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "money": 130, "maximum hit points": 10 diff --git a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift index e875c94..f513955 100644 --- a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift @@ -57,7 +57,6 @@ class PlayerTests: XCTestCase { { "name": "Human", "plural": "Humans", - "minimum age": 18, "lifespan": 90, "base height": "4'8\\"", "height modifier": "2d10", @@ -93,10 +92,7 @@ class PlayerTests: XCTestCase { } // I do the maths - XCTAssertTrue((4.66666...6.33334).contains(player.height.value), "height \(player.height.value)") - - // 110 + 2...8 * 2...20 - XCTAssertTrue((114...270).contains(player.weight.value), "weight \(player.weight.value)") + XCTAssertTrue((4..<7).contains(player.height.value), "height \(player.height.value)") XCTAssertTrue((1...10).contains(player.maximumHitPoints), "maximum hit points") XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") @@ -119,7 +115,6 @@ class PlayerTests: XCTestCase { "class": "Fighter", "gender": "Male", "height": "3'9\\"", - "weight": 120, "ability scores": {"Dexterity": 13, "Charisma": 12}, "background ability scores": ["Strength", "Strength", "Dexterity"], "skills": ["Athletics"], @@ -144,7 +139,6 @@ class PlayerTests: XCTestCase { XCTAssertEqual(player.abilities[.charisma], 12, "charisma") XCTAssertEqual(player.height.value, 3.75, "height") - XCTAssertEqual(player.weight.value, 120, "weight") XCTAssertEqual(player.maximumHitPoints, 10, "maximum hit points") XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") @@ -170,7 +164,6 @@ class PlayerTests: XCTestCase { "class": "Fighter", "alignment": "Lawful Evil", "height": "3'9\\"", - "weight": 120, "ability scores": {"Strength": 12}, "background ability scores": ["Strength", "Strength", "Dexterity"], "skills": ["Athletics"], @@ -217,7 +210,6 @@ class PlayerTests: XCTestCase { "gender": "Male", "alignment": "Neutral Good", "height": "3'9\\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "background ability scores": ["Strength", "Strength", "Dexterity"], "skills": ["Athletics"], @@ -244,7 +236,6 @@ class PlayerTests: XCTestCase { XCTAssertEqual(alignment["morals"], 1, "player traits round trip alignment ethics") } XCTAssertEqual(encoded["height"] as? String, "3.75 ft", "player traits round trip height") - XCTAssertEqual(encoded["weight"] as? String, "120.0 lb", "player traits round trip weight") let abilities = encoded["ability scores"] as? [String: Int] XCTAssertNotNil(abilities) @@ -303,7 +294,6 @@ class PlayerTests: XCTestCase { { "name": "Bilbo", "height": "3'9\\"", - "weight": 120 } """.data(using: .utf8)! let player = try? decoder.decode(Player.self, from: traits) @@ -315,7 +305,6 @@ class PlayerTests: XCTestCase { { "name": "Bilbo", "height": "3'9\\"", - "weight": 120, "ability scores": {"Dexterity": 13} } """.data(using: .utf8)! @@ -328,7 +317,6 @@ class PlayerTests: XCTestCase { { "name": "Bilbo", "height": "3'9\\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "money": 130] } @@ -429,7 +417,6 @@ class PlayerTests: XCTestCase { player2.classTraits = fighter player2.baseAbilities = player1.baseAbilities player2.height = player1.height - player2.weight = player1.weight player2.maximumHitPoints = player1.maximumHitPoints player2.currentHitPoints = player1.currentHitPoints player2.experiencePoints = player1.experiencePoints @@ -466,7 +453,6 @@ class PlayerTests: XCTestCase { let player3 = Player("Boromir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) player3.baseAbilities = player1.baseAbilities player3.height = player1.height - player3.weight = player1.weight player3.money = player1.money player3.currentHitPoints = player3.currentHitPoints - 5 @@ -566,7 +552,6 @@ class PlayerTests: XCTestCase { "flaw": "Impulsive" }, "height": "4'2\\"", - "weight": 95, "ability scores": {"Charisma": 14, "Dexterity": 15}, "background ability scores": ["Strength", "Strength", "Dexterity"], "skills": ["Athletics"], @@ -613,7 +598,6 @@ class PlayerTests: XCTestCase { "species": "Human", "class": "Fighter", "height": "4'2\\"", - "weight": 95, "ability scores": {"Strength": 14}, "background ability scores": ["Strength", "Strength", "Dexterity"], "skills": ["Athletics"], diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift index 4cce908..00f7905 100644 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift @@ -21,12 +21,7 @@ class SpeciesTraitsTests: XCTestCase { { "name": "Human", "plural": "Humans", - "minimum age": 18, "lifespan": 90, - "base height": "4'8\\"", - "height modifier": "2d10", - "base weight": 110, - "weight modifier": "2d4", "speed": 30, "languages": ["Common"], "extra languages": 1 @@ -43,29 +38,11 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertNotNil(speciesTraits) XCTAssertEqual(speciesTraits?.name, "Human", "name") XCTAssertEqual(speciesTraits?.plural, "Humans", "plural") - - XCTAssertEqual(speciesTraits?.minimumAge, 18, "minimum age") + XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") + XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") - XCTAssertEqual(speciesTraits?.baseHeight.value ?? 0, 4.666666, accuracy: 0.000001, "base height") - let heightModifier = speciesTraits?.heightModifier as? SimpleDice - XCTAssertNotNil(heightModifier, "height modifier") - XCTAssertEqual(heightModifier?.sides, 10, "height modifier") - XCTAssertEqual(heightModifier?.times, 2, "height modifier") - - XCTAssertEqual(speciesTraits?.baseWeight.value ?? 0, 110.0, "base height") - - let weightModifier = speciesTraits?.weightModifier as? SimpleDice - XCTAssertNotNil(weightModifier, "weight modifier") - XCTAssertEqual(weightModifier?.sides, 4, "weight modifier") - XCTAssertEqual(weightModifier?.times, 2, "weight modifier") - XCTAssertEqual(speciesTraits?.speed, 30, "speed") - - XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") - - XCTAssertEqual(speciesTraits?.size, SpeciesTraits.Size.medium, "size") - XCTAssertNil(speciesTraits?.alignment, "alignment") } // Test minimum traits @@ -74,11 +51,7 @@ class SpeciesTraitsTests: XCTestCase { { "name": "Giant Human", "plural": "Giant Humans", - "minimum age": 18, "lifespan": 90, - "base height": "7'8\\"", - "height modifier": "2d10", - "base weight": 110, "speed": 30 } """.data(using: .utf8)! @@ -93,26 +66,11 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertEqual(speciesTraits?.name, "Giant Human", "name") XCTAssertEqual(speciesTraits?.plural, "Giant Humans", "plural") - XCTAssertEqual(speciesTraits?.minimumAge, 18, "minimum age") XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") - XCTAssertEqual(speciesTraits?.baseHeight.value ?? 0, 7.666666, accuracy: 0.000001, "base height") - - let heightModifier = speciesTraits?.heightModifier as? SimpleDice - XCTAssertNotNil(heightModifier, "height modifier") - XCTAssertEqual(heightModifier?.sides, 10, "height modifier") - XCTAssertEqual(heightModifier?.times, 2, "height modifier") - - XCTAssertEqual(speciesTraits?.baseWeight.value ?? 0, 110.0, "base height") - - let weightModifier = speciesTraits?.weightModifier as? DiceModifier - XCTAssertEqual(weightModifier?.modifier, 0, "weight modifier") XCTAssertEqual(speciesTraits?.speed, 30, "speed") XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") - XCTAssertNil(speciesTraits?.alignment, "alignment") - - XCTAssertEqual(speciesTraits?.size, SpeciesTraits.Size.large, "size") } // Test optional traits @@ -121,13 +79,8 @@ class SpeciesTraitsTests: XCTestCase { { "name": "Small Human", "plural": "Small Humans", - "minimum age": 18, "lifespan": 90, - "base height": "2'8\\"", - "height modifier": "2d10", - "base weight": 110, "speed": 30, - "alignment": "Lawful Neutral", "aliases": ["Big Human"] } """.data(using: .utf8)! @@ -140,13 +93,6 @@ class SpeciesTraitsTests: XCTestCase { XCTFail("Failed to decode species traits, error: \(error)") } XCTAssertNotNil(speciesTraits) - - XCTAssertEqual(speciesTraits?.size, SpeciesTraits.Size.small, "size") - - XCTAssertNotNil(speciesTraits?.alignment, "alignment should be non-nil") - let foundAlignment = speciesTraits?.alignment?.kind ?? Alignment(.neutral, .neutral).kind - XCTAssertEqual(foundAlignment, Alignment(.lawful, .neutral).kind, "alignment kind") - XCTAssertEqual(speciesTraits?.aliases.count, 1, "aliases count") } } @@ -186,21 +132,13 @@ class SpeciesTraitsTests: XCTestCase { { "name": "Human", "plural": "Humans", - "minimum age": 18, "lifespan": 90, - "base height": "4'8\\"", - "height modifier": "2d10", - "base weight": 110, "speed": 30, "subspecies": [ { "name": "Subhuman", "plural": "Subhumans", - "minimum age": 15, "lifespan": 60, - "base height": "2'8\\"", - "height modifier": "2d6", - "base weight": 45, "speed": 10 } ] @@ -212,29 +150,11 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertEqual(subspeciesTraits.name, "Subhuman", "name") XCTAssertEqual(subspeciesTraits.plural, "Subhumans", "plural") - XCTAssertEqual(subspeciesTraits.minimumAge, 15, "minimum age") XCTAssertEqual(subspeciesTraits.lifespan, 60, "lifespan") - XCTAssertEqual(subspeciesTraits.baseHeight.value, 2.666666, accuracy: 0.000001, "base height") - - let heightModifier = subspeciesTraits.heightModifier as? SimpleDice - XCTAssertNotNil(heightModifier, "height modifier") - XCTAssertEqual(heightModifier?.sides, 6, "height modifier") - XCTAssertEqual(heightModifier?.times, 2, "height modifier") - - XCTAssertEqual(subspeciesTraits.baseWeight.value, 45.0, "base height") - - let weightModifier = subspeciesTraits.weightModifier as? SimpleDice - XCTAssertNil(weightModifier, "weight modifier") - + XCTAssertEqual(subspeciesTraits.speed, 10, "speed") XCTAssertEqual(subspeciesTraits.aliases.count, 0, "aliases") - - XCTAssertEqual(subspeciesTraits.size, SpeciesTraits.Size.small, "size") - XCTAssertNil(subspeciesTraits.alignment, "alignment") - - XCTAssertEqual(subspeciesTraits.hitPointBonus, 0, "hit point bonus") - } else { XCTFail("decode failed for traits with subspecies traits") } @@ -249,21 +169,14 @@ class SpeciesTraitsTests: XCTestCase { { "name": "Human", "plural": "Humans", - "minimum age": 18, "lifespan": 90, - "base height": "4'8\\"", - "height modifier": "2d10", - "base weight": 110, "speed": 30, "subspecies": [ { "name": "Folk", "plural": "Folks", "aliases": ["Plainfolk"], - "weight modifier": "d8", - "alignment": "Neutral", - "darkvision": 20, - "hit point bonus": 2 + "darkvision": 20 } ] } @@ -276,32 +189,13 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertEqual(subspeciesTraits.name, "Folk", "name") XCTAssertEqual(subspeciesTraits.plural, "Folks", "plural") - XCTAssertEqual(subspeciesTraits.minimumAge, 18, "minimum age") XCTAssertEqual(subspeciesTraits.lifespan, 90, "lifespan") - XCTAssertEqual(subspeciesTraits.baseHeight.value, 4.666666, accuracy: 0.000001, "base height") - - let heightModifier = subspeciesTraits.heightModifier as? SimpleDice - XCTAssertNotNil(heightModifier, "height modifier") - XCTAssertEqual(heightModifier?.sides, 10, "height modifier") - XCTAssertEqual(heightModifier?.times, 2, "height modifier") - - XCTAssertEqual(subspeciesTraits.baseWeight.value, 110, "base height") - - let weightModifier = subspeciesTraits.weightModifier as? SimpleDice - XCTAssertNotNil(weightModifier, "weight modifier") - XCTAssertEqual(weightModifier?.sides, 8, "weight modifier") - XCTAssertEqual(weightModifier?.times, 1, "weight modifier") - + XCTAssertEqual(subspeciesTraits.speed, 30, "speed") XCTAssertEqual(subspeciesTraits.aliases.count, 1, "aliases") - XCTAssertEqual(subspeciesTraits.size, SpeciesTraits.Size.medium, "size") - XCTAssertNotNil(subspeciesTraits.alignment, "alignment") - let foundAlignment = subspeciesTraits.alignment?.kind ?? Alignment(.lawful, .good).kind - XCTAssertEqual(foundAlignment, Alignment(.neutral, .neutral).kind, "alignment kind") - - XCTAssertEqual(subspeciesTraits.hitPointBonus, 2, "hit point bonus") + XCTAssertEqual(subspeciesTraits.baseSizes, speciesTraits.baseSizes, "size") } else { XCTFail("decode failed for traits with subspecies traits") } @@ -312,13 +206,13 @@ class SpeciesTraitsTests: XCTestCase { } func testEncodingSubspeciesTraits() { - let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], descriptiveTraits: [:], minimumAge: 18, lifespan: 90, alignment: Alignment(.lawful, .neutral), baseHeight: "4ft 9 in".parseHeight!, heightModifier: DiceModifier(0), baseWeight: "178 lb".parseWeight!, weightModifier: DiceModifier(0), darkVision: 0, speed: 45, hitPointBonus: 0) + let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], descriptiveTraits: [:], lifespan: 90, darkVision: 0, speed: 45) let encoder = JSONEncoder() do { var copyOfSpeciesTraits = speciesTraits - var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", minimumAge: 14, lifespan: 45, baseHeight: "3 ft".parseHeight!, baseWeight: "100 lb".parseWeight!, darkVision: 0, speed: 30) + var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", lifespan: 45, darkVision: 0, speed: 30) subspeciesTraits.blendTraits(from: copyOfSpeciesTraits) copyOfSpeciesTraits.subspecies.append(subspeciesTraits) @@ -329,35 +223,20 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertEqual(dictionary["name"] as? String, "Human", "encoding name") XCTAssertEqual(dictionary["plural"] as? String, "Humans", "encoding name") - XCTAssertEqual(dictionary["minimum age"] as? Int, 18, "encoding name") XCTAssertEqual(dictionary["lifespan"] as? Int, 90, "encoding lifespan") - XCTAssertEqual(dictionary["alignment"] as? String, "Lawful Neutral", "encoding alignment") - XCTAssertEqual(dictionary["base height"]! as! String, "4.75 ft", "encoding base height") - XCTAssertEqual(dictionary["height modifier"] as? String, "0", "encoding height modifier") - XCTAssertEqual(dictionary["base weight"]! as! String, "178.0 lb", "encoding base weight") - XCTAssertEqual(dictionary["weight modifier"] as? String, "0", "encoding weight modifier") XCTAssertEqual(dictionary["darkvision"] as? Int, 0, "encoding name") XCTAssertEqual(dictionary["speed"] as? Int, 45, "encoding name") - XCTAssertEqual(dictionary["hit point bonus"] as? Int, 0, "encoding base height") // Confirm subspecies traits if let subspecies = dictionary["subspecies"] as? [[String: Any]], let firstSubspecies = subspecies.first { XCTAssertEqual(firstSubspecies["name"] as? String, "Subhuman", "encoding name") XCTAssertEqual(firstSubspecies["plural"] as? String, "Subhumans", "encoding name") - XCTAssertEqual(firstSubspecies["minimum age"] as? Int, 14, "encoding name") XCTAssertEqual(firstSubspecies["lifespan"] as? Int, 45, "encoding lifespan") - XCTAssertNil(firstSubspecies["alignment"], "encoding alignment") - XCTAssertEqual(firstSubspecies["base height"]! as! String, "3.0 ft", "encoding base height") - XCTAssertNil(firstSubspecies["height modifier"], "encoding height modifier") - XCTAssertEqual(firstSubspecies["base weight"]! as! String, "100.0 lb", "encoding base weight") - XCTAssertNil(firstSubspecies["weight modifier"], "encoding weight modifier") XCTAssertNil(firstSubspecies["darkvision"], "encoding darkvision") XCTAssertEqual(firstSubspecies["speed"] as? Int, 30, "encoding speed") - XCTAssertNil(firstSubspecies["hit point bonus"], "encoding hit point bonus") - } else { XCTFail("subspecies should be non-nil and contain at least one subspecies") } @@ -368,7 +247,7 @@ class SpeciesTraitsTests: XCTestCase { do { var copyOfSpeciesTraits = speciesTraits - let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], descriptiveTraits: ["background": "Something"], minimumAge: 14, lifespan: 45, alignment: Alignment(.neutral, .evil), baseHeight: "3 ft".parseHeight!, heightModifier: "d4".parseDice!, baseWeight: "100 lb".parseWeight!, weightModifier: "d6".parseDice!, darkVision: 10, speed: 45, hitPointBonus: 1) + let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], descriptiveTraits: ["background": "Something"], lifespan: 45, darkVision: 10, speed: 45) copyOfSpeciesTraits.subspecies.append(subspeciesTraits) let encoded = try encoder.encode(copyOfSpeciesTraits) @@ -379,17 +258,10 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertEqual(firstSubspecies["name"] as? String, "Subhuman", "encoding name") XCTAssertEqual(firstSubspecies["plural"] as? String, "Subhumans", "encoding name") - XCTAssertEqual(firstSubspecies["minimum age"] as? Int, 14, "encoding name") XCTAssertEqual(firstSubspecies["lifespan"] as? Int, 45, "encoding lifespan") - XCTAssertEqual(firstSubspecies["alignment"] as? String, "Neutral Evil", "encoding alignment") - XCTAssertEqual(firstSubspecies["base height"]! as! String, "3.0 ft", "encoding base height") - XCTAssertEqual(firstSubspecies["height modifier"] as? String, "d4", "encoding height modifier") - XCTAssertEqual(firstSubspecies["base weight"]! as! String, "100.0 lb", "encoding base weight") - XCTAssertEqual(firstSubspecies["weight modifier"] as? String, "d6", "encoding weight modifier") XCTAssertEqual(firstSubspecies["darkvision"] as? Int, 10, "encoding darkvision") XCTAssertNil(firstSubspecies["speed"], "encoding speed") - XCTAssertEqual(firstSubspecies["hit point bonus"] as? Int, 1, "encoding hit point bonus") } else { XCTFail("subspecies should be non-nil and contain at least one subspecies") } diff --git a/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json b/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json index bf84287..8269a7f 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json @@ -5,11 +5,6 @@ "plural": "Dragonborn", "minimum age": 15, "lifespan": 80, - "alignment": "Good", - "base height": "5'6\"", - "height modifier": "2d8", - "base weight": 175, - "weight modifier": "2d6", "speed": 30, "skill proficiencies": ["draconic ancestry", "breath weapon", "damage"], "languages": ["Common", "Draconic"] @@ -17,12 +12,7 @@ { "name": "Gnome", "plural": "Gnomes", - "minimum age": 40, "lifespan": 425, - "alignment": "Good", - "base height": "2'11\"", - "height modifier": "2d4", - "base weight": 35, "speed": 25, "darkvision": 60, "skill proficiencies": ["gnome cunning"], @@ -41,14 +31,7 @@ { "name": "Half-Elf", "plural": "Half-Elves", - "ability score bonuses": 2, - "minimum age": 20, "lifespan": 180, - "alignment": "Chaotic", - "base height": "4'9\"", - "height modifier": "2d8", - "base weight": 110, - "weight modifier": "2d4", "speed": 30, "darkvision": 60, "skill proficiencies": ["fey ancestry"], @@ -59,13 +42,7 @@ { "name": "Half-Orc", "plural": "Half-Orcs", - "minimum age": 14, "lifespan": 75, - "alignment": "Chaotic", - "base height": "4'10\"", - "height modifier": "2d10", - "base weight": 140, - "weight modifier": "2d6", "speed": 30, "darkvision": 60, "skill proficiencies": ["menacing", "relentless endurance", "savage attacks"], @@ -74,13 +51,7 @@ { "name": "Tiefling", "plural": "Tieflings", - "minimum age": 18, "lifespan": 100, - "alignment": "Chaotic", - "base height": "4'9\"", - "height modifier": "2d8", - "base weight": 110, - "weight modifier": "2d4", "speed": 30, "darkvision": 60, "skill proficiencies": ["hellish resistance", "infernal legacy"], diff --git a/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json b/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json index 5f9dd79..c8df705 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestPlayers.json @@ -7,7 +7,6 @@ "class": "Cleric", "species": "High Elf", "height": "3'9\"", - "weight": 120, "ability scores": {"Dexterity": 13}, "background ability scores": ["Strength", "Strength", "Dexterity"], "skills": ["Athletics"], @@ -22,7 +21,6 @@ "species": "Human", "alignment": "Lawful Evil", "height": "3'9\"", - "weight": 120, "ability scores": {"Strength": 12}, "background ability scores": ["Strength", "Dexterity", "Dexterity"], "skills": ["Athletics"], diff --git a/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json b/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json index 9ff4b47..9cd9a1b 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestSpecies.json @@ -1,116 +1,67 @@ { "species": [ - { - "name": "Dwarf", - "plural": "Dwarves", - "minimum age": 50, - "lifespan": 350, - "alignment": "Lawful Good", - "base height": 4, - "height modifier": "2d4", - "base weight": 130, - "weight modifier": "2d6", - "speed": 25, - "darkvision": 60, - "skill proficiencies": ["dwarven resilience"], - "weapon proficiencies" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], - "tools": ["smith's tools", "brewer's supplies", "mason's tools"], - "history": ["stonecunning"], - "languages": ["Common", "Dwarvish"], - "subspecies": [{ - "name": "Hill Dwarf", - "plural": "Hill Dwarves", - "hit point bonus": 1, - "base height": "3'5\"", - "base weight": 115, - }, - { - "name": "Mountain Dwarf", - "plural": "Mountain Dwarves", - "armor training": ["light", "medium"], - }] - }, - { - "name": "Elf", - "plural": "Elves", - "minimum age": 100, - "lifespan": 750, - "alignment": "Chaotic Good", - "base height": "4'6\"", - "height modifier": "2d10", - "base weight": 100, - "weight modifier": "1d4", - "speed": 30, - "darkvision": 60, - "resilience": ["poison", "poison damage"], - "skill proficiencies": ["keen senses", "fey ancestry", "trance"], - "languages": ["Common", "Elvish"], - "subspecies": [{ - "name": "High Elf", - "plural": "High Elves", - "base weight:": 90, - "weapon proficiencies": ["longsword", "shortsword", "shortbow", "longbow"], - "extra languages": 1, - "spells": ["wizard cantrip"], - "hit point bonus": 1 - }, - { - "name": "Wood Elf", - "plural": "Wood Elves", - "weapon proficiencies": ["longsword", "shortsword", "shortbow", "longbow"], - "skill proficiencies": ["fleet of foot", "mask of the wild"] - }, - { - "name": "Dark Elf", - "plural": "Dark Elves", - "aliases": ["Drow"], - "base height": "4'5\"", - "height modifier": "2d6", - "base weight": 75, - "weight modifier": "1d6", - "darkvision": 120, - "weapon proficiencies": ["rapier", "shortsword", "hand crossbow"], - "spells": ["dancing lights", "faerie fire:3", "darkness:5"] - }] - }, - { - "name": "Halfling", - "plural": "Halflings", - "minimum age": 20, - "lifespan": 150, - "alignment": "Lawful Good", - "base height": "2'7\"", - "base weight": 35, - "height modifier": "2d4", - "speed": 25, - "skill proficiencies": ["lucky", "brave", "halfling nimbleness"], - "weapon proficiencies" : ["battleaxe", "handaxe", "throwing hammer", "war hammer"], - "tools": ["smith's tools", "brewer's supplies", "mason's tools"], - "history": ["stonecunning"], - "languages": ["Common", "Halfling"], - "subspecies": [{ - "name": "Lightfoot", - "plural": "Lightfoots", - "skill proficiencies": ["stealthy"] - }, - { - "name": "Stout", - "plural": "Stouts", - "skill proficiencies": ["stout resilience"] - }] - }, - { - "name": "Human", - "plural": "Humans", - "minimum age": 18, - "lifespan": 90, - "base height": "4'8\"", - "height modifier": "2d10", - "base weight": 110, - "weight modifier": "2d4", - "speed": 30, - "languages": ["Common"], - "extra languages": 1 - } + { + "name": "Dwarf", + "plural": "Dwarves", + "lifespan": 350, + "base sizes": ["4-5 ft"], + "speed": 30, + "darkvision": 120, + "subspecies": [{ + "name": "Hill Dwarf", + "plural": "Hill Dwarves", + }, + { + "name": "Mountain Dwarf", + "plural": "Mountain Dwarves", + }] + }, + { + "name": "Elf", + "plural": "Elves", + "lifespan": 750, + "base sizes": ["5-6 ft"], + "speed": 30, + "darkvision": 60, + "subspecies": [{ + "name": "High Elf", + "plural": "High Elves" + }, + { + "name": "Wood Elf", + "plural": "Wood Elves" + }, + { + "name": "Drow", + "plural": "Drow", + "aliases": ["Dark Elf"], + "darkvision": 120 + }] + }, + { + "name": "Halfling", + "plural": "Halflings", + "lifespan": 150, + "base sizes": ["2-3 ft"], + "speed": 30, + "subspecies": [{ + "name": "Lightfoot", + "plural": "Lightfoots", + "aliases": ["Tallfellow"] + }, + { + "name": "Stout", + "plural": "Stouts", + "aliases": ["Strongheart"] + }] + + }, + { + "name": "Human", + "plural": "Humans", + "lifespan": 80, + "base sizes": ["4-7 ft", "2-4 ft"], + "speed": 30 + } ] } From bad93ec2c44a9384f9e19e1293d2547a48aedf60 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Tue, 28 Oct 2025 15:54:57 -0400 Subject: [PATCH 03/33] Removed some whitespace. Increased deployment target to iOS 18 or later. Replaced usage of floor with rounded. Broke out unit test points. --- .../project.pbxproj | 12 +- .../RolePlayingCore.xcodeproj/project.pbxproj | 16 +- .../RolePlayingCore/Common/Height.swift | 29 +- .../RolePlayingCore/Common/JSONFile.swift | 1 - .../Common/NameGenerator.swift | 1 - .../RolePlayingCore/Common/Weight.swift | 2 - .../Configuration/CharacterGenerator.swift | 2 - .../Configuration/Configuration.swift | 2 - .../RolePlayingCore/Currency/Currencies.swift | 2 - .../RolePlayingCore/Currency/Money.swift | 1 - .../Currency/UnitCurrency.swift | 1 - .../RolePlayingCore/Dice/CompoundDice.swift | 2 - .../RolePlayingCore/Dice/Dice.swift | 1 - .../RolePlayingCore/Dice/DiceModifier.swift | 2 - .../RolePlayingCore/Dice/DiceParser.swift | 3 - .../RolePlayingCore/Dice/DiceRoll.swift | 3 +- .../RolePlayingCore/Dice/Die.swift | 2 - .../RolePlayingCore/Dice/DroppingDice.swift | 2 - .../RolePlayingCore/Dice/SimpleDice.swift | 2 - .../RolePlayingCore/Player/Ability.swift | 14 +- .../RolePlayingCore/Player/Alignment.swift | 8 - .../RolePlayingCore/Player/Backgrounds.swift | 3 +- .../RolePlayingCore/Player/ClassTraits.swift | 1 - .../RolePlayingCore/Player/Classes.swift | 2 +- .../RolePlayingCore/Player/Player.swift | 6 +- .../RolePlayingCore/Player/Players.swift | 3 - .../RolePlayingCore/Player/Species.swift | 1 - .../Player/SpeciesTraits.swift | 1 - .../RolePlayingCoreTests/AbilityTests.swift | 293 ++++++------- .../RolePlayingCoreTests/AlignmentTests.swift | 406 +++++++----------- .../ConfigurationTests.swift | 1 - .../RolePlayingCoreTests/CurrencyTests.swift | 1 - .../DiceParserTests.swift | 1 - .../RolePlayingCoreTests/DiceTests.swift | 4 - .../RolePlayingCoreTests/HeightTests.swift | 15 +- .../RolePlayingCoreTests/PlayerTests.swift | 216 +++++----- .../RolePlayingCoreTests.swift | 20 - .../ServiceErrorTests.swift | 2 - .../RolePlayingCoreTests/SpeciesTests.swift | 1 - .../SpeciesTraitsTests.swift | 155 +++---- .../RolePlayingCoreTests/WeightTests.swift | 12 - 41 files changed, 495 insertions(+), 757 deletions(-) delete mode 100644 RolePlayingCore/RolePlayingCoreTests/RolePlayingCoreTests.swift diff --git a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj index 6d815bb..ac82b46 100644 --- a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj +++ b/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj @@ -513,7 +513,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = J69E69SP27; INFOPLIST_FILE = CharacterGenerator/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -531,7 +531,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = J69E69SP27; INFOPLIST_FILE = CharacterGenerator/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -549,7 +549,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = J69E69SP27; INFOPLIST_FILE = CharacterGeneratorTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -569,7 +569,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = J69E69SP27; INFOPLIST_FILE = CharacterGeneratorTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -591,7 +591,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = J69E69SP27; INFOPLIST_FILE = CharacterGeneratorUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -613,7 +613,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = J69E69SP27; INFOPLIST_FILE = CharacterGeneratorUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj index c4437c8..662825d 100644 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj +++ b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ B62055F81E19DD23002494AB /* RolePlayingCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B62055EE1E19DD23002494AB /* RolePlayingCore.framework */; }; - B62055FD1E19DD23002494AB /* RolePlayingCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62055FC1E19DD23002494AB /* RolePlayingCoreTests.swift */; }; B62055FF1E19DD23002494AB /* RolePlayingCore.h in Headers */ = {isa = PBXBuildFile; fileRef = B62055F11E19DD23002494AB /* RolePlayingCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; B620560A1E19DDC0002494AB /* DiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62056091E19DDC0002494AB /* DiceTests.swift */; }; B620560E1E19DDD0002494AB /* Dice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B620560B1E19DDD0002494AB /* Dice.swift */; }; @@ -99,7 +98,6 @@ B62055F11E19DD23002494AB /* RolePlayingCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RolePlayingCore.h; sourceTree = ""; }; B62055F21E19DD23002494AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B62055F71E19DD23002494AB /* RolePlayingCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RolePlayingCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B62055FC1E19DD23002494AB /* RolePlayingCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RolePlayingCoreTests.swift; sourceTree = ""; }; B62055FE1E19DD23002494AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B62056091E19DDC0002494AB /* DiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceTests.swift; sourceTree = ""; }; B620560B1E19DDD0002494AB /* Dice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dice.swift; sourceTree = ""; }; @@ -229,7 +227,6 @@ B62055FB1E19DD23002494AB /* RolePlayingCoreTests */ = { isa = PBXGroup; children = ( - B62055FC1E19DD23002494AB /* RolePlayingCoreTests.swift */, B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */, B6F070351E4F991D00F66918 /* AlignmentTests.swift */, B626FA622EAF9F9200359F01 /* BackgroundsTests.swift */, @@ -327,16 +324,16 @@ children = ( B6FA6CC41E4A96D1004D91B1 /* Ability.swift */, B6FA6CCA1E4E7AEA004D91B1 /* Alignment.swift */, - B626FA582EAE76AA00359F01 /* Skill.swift */, B626FA5A2EAE802D00359F01 /* BackgroundTraits.swift */, B626FA5C2EAE81C600359F01 /* Backgrounds.swift */, B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */, B6CF53CF1E54E2E200CADD9F /* Classes.swift */, - B69F84681E58B8F700A4D2B0 /* Player.swift */, B6688B502EACF5AE000A83DD /* Initiative.swift */, + B69F84681E58B8F700A4D2B0 /* Player.swift */, B69F846C1E58D66900A4D2B0 /* Players.swift */, B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */, B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */, + B626FA582EAE76AA00359F01 /* Skill.swift */, B6CF53D11E54E2EF00CADD9F /* Species.swift */, ); path = Player; @@ -533,7 +530,6 @@ B6FA6CB61E47B080004D91B1 /* CurrencyTests.swift in Sources */, B620560A1E19DDC0002494AB /* DiceTests.swift in Sources */, B6FA6CBF1E47C2F9004D91B1 /* HeightTests.swift in Sources */, - B62055FD1E19DD23002494AB /* RolePlayingCoreTests.swift in Sources */, B6FA6CC11E47C306004D91B1 /* WeightTests.swift in Sources */, B66AF9071EAE88FF00C15F8E /* ConfigurationTests.swift in Sources */, B69F846B1E58D33900A4D2B0 /* PlayerTests.swift in Sources */, @@ -688,7 +684,7 @@ ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = RolePlayingCore/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -717,7 +713,7 @@ ENABLE_MODULE_VERIFIER = YES; INFOPLIST_FILE = RolePlayingCore/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -737,7 +733,7 @@ buildSettings = { DEVELOPMENT_TEAM = J69E69SP27; INFOPLIST_FILE = RolePlayingCoreTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -754,7 +750,7 @@ buildSettings = { DEVELOPMENT_TEAM = J69E69SP27; INFOPLIST_FILE = RolePlayingCoreTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.6; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/RolePlayingCore/RolePlayingCore/Common/Height.swift b/RolePlayingCore/RolePlayingCore/Common/Height.swift index 4ffe5e1..8946c98 100644 --- a/RolePlayingCore/RolePlayingCore/Common/Height.swift +++ b/RolePlayingCore/RolePlayingCore/Common/Height.swift @@ -117,25 +117,16 @@ extension Height { /// Returns a string representation of the height suitable for display. public var displayString: String { - let locale = Locale.current - let usesMetric = locale.measurementSystem == .metric + // Use feet and inches for imperial locales + let totalInches = self.converted(to: .inches).value + let feet = Int(totalInches / 12) + let inches = Int(totalInches.truncatingRemainder(dividingBy: 12)) - if usesMetric { - // Use centimeters for metric locales - let cm = self.converted(to: .centimeters) - return cm.formatted(.measurement(width: .abbreviated, usage: .personHeight)) - } else { - // Use feet and inches for imperial locales - let totalInches = self.converted(to: .inches).value - let feet = Int(totalInches / 12) - let inches = Int(totalInches.truncatingRemainder(dividingBy: 12)) - - let feetMeasurement = Measurement(value: Double(feet), unit: UnitLength.feet) - let feetString = feetMeasurement.formatted(.measurement(width: .abbreviated)) - - let inchesMeasurement = Measurement(value: Double(inches), unit: UnitLength.inches) - let inchesString = inchesMeasurement.formatted(.measurement(width: .abbreviated)) - return "\(feetString), \(inchesString)" - } + let feetMeasurement = Measurement(value: Double(feet), unit: UnitLength.feet) + let feetString = feetMeasurement.formatted(.measurement(width: .abbreviated)) + + let inchesMeasurement = Measurement(value: Double(inches), unit: UnitLength.inches) + let inchesString = inchesMeasurement.formatted(.measurement(width: .abbreviated)) + return "\(feetString), \(inchesString)" } } diff --git a/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift b/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift index 8e9bbea..5fde359 100644 --- a/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift +++ b/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift @@ -19,5 +19,4 @@ extension Bundle { guard let url = self.url(forResource: fileName, withExtension: "json") else { throw RuntimeError("Could not load \(fileName).json from \(self.bundleURL)") } return try Data(contentsOf: url, options: [.mappedIfSafe]) } - } diff --git a/RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift b/RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift index 52a5742..055e9ae 100644 --- a/RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift +++ b/RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift @@ -11,7 +11,6 @@ /// This implements a Markov-Chain-based name generator. /// See for a description. public struct NameGenerator { - private var nameStarters: [String] private var nameParts: [String: [String]] diff --git a/RolePlayingCore/RolePlayingCore/Common/Weight.swift b/RolePlayingCore/RolePlayingCore/Common/Weight.swift index ff7c033..fd84c89 100644 --- a/RolePlayingCore/RolePlayingCore/Common/Weight.swift +++ b/RolePlayingCore/RolePlayingCore/Common/Weight.swift @@ -39,7 +39,6 @@ extension String { return Weight(value: value!, unit: unit) } - } public extension KeyedDecodingContainer { @@ -81,7 +80,6 @@ public extension KeyedDecodingContainer { return weight } - } extension Weight { diff --git a/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift b/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift index 5aa2df8..b431c02 100644 --- a/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift +++ b/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift @@ -11,9 +11,7 @@ import Foundation /// Given a configuration of species traits and class traits, /// provides a random character. public struct CharacterGenerator { - let configuration: Configuration - let names: SpeciesNames /// Creates a character generator instance with a reference to the current configuration. diff --git a/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift b/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift index a33687b..09df5d6 100644 --- a/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift +++ b/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift @@ -32,7 +32,6 @@ public struct ConfigurationFiles: Decodable { /// This is designed to configure a client from a framework or application bundle. public struct Configuration { - let bundle: Bundle public var configurationFiles: ConfigurationFiles @@ -94,5 +93,4 @@ public struct Configuration { } } } - } diff --git a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift b/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift index 85cc190..8ee70ec 100644 --- a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift +++ b/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift @@ -31,7 +31,6 @@ public struct Currencies { UnitCurrency.baseUnitCurrency = newBaseUnit } - } extension Currencies: Codable { @@ -106,5 +105,4 @@ extension Currencies: Codable { try container.encode(currencies, forKey: .currencies) } - } diff --git a/RolePlayingCore/RolePlayingCore/Currency/Money.swift b/RolePlayingCore/RolePlayingCore/Currency/Money.swift index 860d457..6e96da7 100644 --- a/RolePlayingCore/RolePlayingCore/Currency/Money.swift +++ b/RolePlayingCore/RolePlayingCore/Currency/Money.swift @@ -95,5 +95,4 @@ extension MeasurementFormatter { let formatString = unitStyle == .short ? "%@%@" : "%@ %@" return String(format: formatString, valueString, unitsString!) } - } diff --git a/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift b/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift index 3c51883..7cb102a 100644 --- a/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift +++ b/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift @@ -37,5 +37,4 @@ public final class UnitCurrency : Dimension, @unchecked Sendable { public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift b/RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift index 3bc3e4c..a12d389 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift +++ b/RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift @@ -15,7 +15,6 @@ import Foundation /// - combining two rolls, e.g., "2d4+d6", /// - using a modifier, e.g., "d12+2". public struct CompoundDice: Dice { - public let lhs: Dice public let rhs: Dice public let mathOperator: String @@ -59,5 +58,4 @@ public struct CompoundDice: Dice { /// Returns a description of the left and right hand sides with the math operator. public var description: String { "\(lhs)\(mathOperator)\(rhs)" } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/Dice.swift b/RolePlayingCore/RolePlayingCore/Dice/Dice.swift index f3f3e36..a18c7ad 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/Dice.swift +++ b/RolePlayingCore/RolePlayingCore/Dice/Dice.swift @@ -16,5 +16,4 @@ public protocol Dice: CustomStringConvertible { /// Returns the number of dice sides. var sides: Int { get } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift b/RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift index caf3ae0..bc8266b 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift +++ b/RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift @@ -9,7 +9,6 @@ /// A dice modifier is a constant value that can take the place of a Dice instance. public struct DiceModifier: Dice { - public let modifier: Int public init(_ modifier: Int) { @@ -23,5 +22,4 @@ public struct DiceModifier: Dice { public var sides: Int { modifier } public var description: String { "\(modifier)" } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift b/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift index 88a6cdd..3d43b23 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift +++ b/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift @@ -8,7 +8,6 @@ import Foundation - // TODO: the initial implementation performed naïve sub-string searches, and was very limited. // This implementation uses a lightweight tokenizer and parser, and is a lot more robust. // A smaller implementation might leverage regular expression, but may be harder to maintain. @@ -253,7 +252,6 @@ public extension String { return dice } - } // TODO: this works around the fact that we can't explicitly make the Dice protocol Decodable. @@ -301,5 +299,4 @@ public extension KeyedDecodingContainer { return dice } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift b/RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift index 3ddccde..221bb67 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift +++ b/RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift @@ -8,7 +8,7 @@ /// Encapsulates a result with its intermediate values. public struct DiceRoll: CustomStringConvertible { - + /// The result of the roll. public let result: Int @@ -21,6 +21,5 @@ public struct DiceRoll: CustomStringConvertible { self.result = result self.description = description } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/Die.swift b/RolePlayingCore/RolePlayingCore/Dice/Die.swift index 38d390d..3c43856 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/Die.swift +++ b/RolePlayingCore/RolePlayingCore/Dice/Die.swift @@ -37,12 +37,10 @@ public enum Die: Int { } return rolls } - } extension Die: CustomStringConvertible { /// Returns the number of die sides with "d" prepended. public var description: String { rawValue == 100 ? "d%" : "d\(rawValue)" } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/DroppingDice.swift b/RolePlayingCore/RolePlayingCore/Dice/DroppingDice.swift index b4871ce..49a54f2 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/DroppingDice.swift +++ b/RolePlayingCore/RolePlayingCore/Dice/DroppingDice.swift @@ -10,7 +10,6 @@ /// A dropping dice is an extension of SimpleDice that drops the highest or lowest roll. /// This is done through composition, instead of subclassing. public struct DroppingDice: Dice { - public let dice: SimpleDice /// Options to drop the lowest or highest roll. @@ -55,5 +54,4 @@ public struct DroppingDice: Dice { public var description: String { return "\(dice)-\(drop.rawValue)" } - } diff --git a/RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift b/RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift index d17b64d..1271f25 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift +++ b/RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift @@ -9,7 +9,6 @@ /// A simple dice has a single die, and an optional number of times to roll. public struct SimpleDice: Dice { - public let die: Die public let times: Int @@ -62,5 +61,4 @@ public struct SimpleDice: Dice { return resultString } - } diff --git a/RolePlayingCore/RolePlayingCore/Player/Ability.swift b/RolePlayingCore/RolePlayingCore/Player/Ability.swift index e1a829e..3ed768a 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Ability.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Ability.swift @@ -7,14 +7,12 @@ // public struct Ability { - public let name: String /// Creates an ability name. public init(_ name: String) { self.name = name } - } extension Ability: Equatable { } @@ -28,7 +26,6 @@ extension String { let index = self.index(self.startIndex, offsetBy: min(self.count, 3)) return self[..(using generator: inout G) -> BackgroundTraits { return backgrounds.randomElementByIndex(using: &generator)! } - } diff --git a/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift b/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift index a5f3a77..8e297ed 100644 --- a/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift +++ b/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift @@ -9,7 +9,6 @@ import Foundation public struct ClassTraits { - public var name: String public var plural: String public var hitDice: Dice diff --git a/RolePlayingCore/RolePlayingCore/Player/Classes.swift b/RolePlayingCore/RolePlayingCore/Player/Classes.swift index fc33862..1498e56 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Classes.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Classes.swift @@ -6,7 +6,7 @@ // Copyright © 2016 Brian Arnold. All rights reserved. // -// A set of class traits +// A collection of class traits public struct Classes: Codable { public var classes = [ClassTraits]() diff --git a/RolePlayingCore/RolePlayingCore/Player/Player.swift b/RolePlayingCore/RolePlayingCore/Player/Player.swift index 0f0bad3..5a2ad89 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Player.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Player.swift @@ -17,7 +17,6 @@ public extension AbilityScores { scores[ability] = dice.roll().result } } - } public extension Dice { @@ -30,7 +29,6 @@ public extension Dice { // TODO: Is this a base class for Character? What about NPC? Monster? Should we have a protocol? public class Player: Codable { - public var name: String public var descriptiveTraits: [String: String] // ideals, bonds, flaws, background @@ -254,9 +252,8 @@ public class Player: Codable { maximumHitPoints += rollHitPoints() - // TODO: add more for leveling up + // TODO: add more details for leveling up } - } extension Player: Hashable { @@ -293,5 +290,4 @@ extension Player: Hashable { hasher.combine(level) hasher.combine(money) } - } diff --git a/RolePlayingCore/RolePlayingCore/Player/Players.swift b/RolePlayingCore/RolePlayingCore/Player/Players.swift index 3ec5cd3..d6aab80 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Players.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Players.swift @@ -33,11 +33,9 @@ extension Player { } self.classTraits = classTraits } - } public class Players: Codable { - public var players = [Player]() public func resolve(backgrounds: Backgrounds, classes: Classes, species: Species) throws { @@ -65,5 +63,4 @@ public class Players: Codable { public func remove(at index: Int) { players.remove(at: index) } - } diff --git a/RolePlayingCore/RolePlayingCore/Player/Species.swift b/RolePlayingCore/RolePlayingCore/Player/Species.swift index 8cb0fc4..9dd83c3 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Species.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Species.swift @@ -64,5 +64,4 @@ public class Species: Codable { self.species = species } - } diff --git a/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift b/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift index 1f1ac45..23e5748 100644 --- a/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift +++ b/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift @@ -184,5 +184,4 @@ extension SpeciesTraits: Codable { try values.encode(self.speed, forKey: .speed) } } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift b/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift index 22b7124..b819ed2 100644 --- a/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift @@ -13,20 +13,14 @@ import XCTest class AbilityTests: XCTestCase { func testStringAbbreviation() { - do { - let string = "Strength" - XCTAssertEqual(string.abbreviated, "STR", "Strength abbreviated") - } - - do { - let string = "a" - XCTAssertEqual(string.abbreviated, "A", "A abbreviated") - } - - do { - let empty = "" - XCTAssertEqual(empty.abbreviated, "", "empty abbreviated") - } + let strength = "Strength" + XCTAssertEqual(strength.abbreviated, "STR", "Strength abbreviated") + + let lettera = "a" + XCTAssertEqual(lettera.abbreviated, "A", "A abbreviated") + + let empty = "" + XCTAssertEqual(empty.abbreviated, "", "empty abbreviated") } func testIntScoreModifier() { @@ -144,21 +138,19 @@ class AbilityTests: XCTestCase { func testAbilityScoresDecodable() { // Test with implicit [String: Int] as from JSON - do { - let traits = """ - {"Strength": 12, "Intelligence": 8} - """.data(using: .utf8)! - - let decoder = JSONDecoder() - let abilityScores = try? decoder.decode(AbilityScores.self, from: traits) - XCTAssertNotNil(abilityScores, "ability scores should be non-nil") - - let strength = Ability("Strength") - let intelligence = Ability("Intelligence") - - XCTAssertEqual(abilityScores?[strength], 12, "ability scores dictionary strength") - XCTAssertEqual(abilityScores?[intelligence], 8, "ability scores dictionary intelligence") - } + let traits = """ + {"Strength": 12, "Intelligence": 8} + """.data(using: .utf8)! + + let decoder = JSONDecoder() + let abilityScores = try? decoder.decode(AbilityScores.self, from: traits) + XCTAssertNotNil(abilityScores, "ability scores should be non-nil") + + let strength = Ability("Strength") + let intelligence = Ability("Intelligence") + + XCTAssertEqual(abilityScores?[strength], 12, "ability scores dictionary strength") + XCTAssertEqual(abilityScores?[intelligence], 8, "ability scores dictionary intelligence") } func testAbilityScoresEncodable() { @@ -180,151 +172,132 @@ class AbilityTests: XCTestCase { func testAbilityScoreKey() { // Housekeeping: code coverage for AbilityKey - do { - let abilityKey = AbilityScores.AbilityKey(stringValue: "Wisdom")! - XCTAssertNil(abilityKey.intValue, "AbilityKey does not use intValue") - } + let wisdomKey = AbilityScores.AbilityKey(stringValue: "Wisdom")! + XCTAssertNil(wisdomKey.intValue, "AbilityKey does not use intValue") + + let intKey = AbilityScores.AbilityKey(intValue: 2) + XCTAssertNil(intKey, "AbilityKey does not use intValue") + } + + func testAddingModifiers() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") - do { - let abilityKey = AbilityScores.AbilityKey(intValue: 2) - XCTAssertNil(abilityKey, "AbilityKey does not use intValue") - } + let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + let combinedScores = abilityScores + abilityScores.modifiers + + XCTAssertEqual(combinedScores[brawn], 7, "adding ability scores brawn") + XCTAssertEqual(combinedScores[reflexes], 14, "adding ability scores reflexes") + XCTAssertEqual(combinedScores[stamina], 20, "adding ability scores stamina") } - func testAddingAbilityScores() { - // Test adding scores with modifiers using = ... + ... - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - let combinedScores = abilityScores + abilityScores.modifiers - - XCTAssertEqual(combinedScores[brawn], 7, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 14, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 20, "adding ability scores stamina") - } + func testAddingOneScore() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") - // Test adding one score using += ... - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - let oneScore = AbilityScores([reflexes: -3]) - abilityScores += oneScore - - XCTAssertEqual(abilityScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(abilityScores[reflexes], 10, "adding ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 17, "adding ability scores stamina") - } - - // Test that adding unrelated scores has no effect - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - - let intelligence = Ability("Intelligence") - let wisdom = Ability("Wisdom") - let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) - let combinedScores = abilityScores + unrelatedScores - - XCTAssertEqual(combinedScores.count, 3, "adding ability scores count") - - XCTAssertEqual(combinedScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 13, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 17, "adding ability scores stamina") - } + var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + let oneScore = AbilityScores([reflexes: -3]) + abilityScores += oneScore + + XCTAssertEqual(abilityScores[brawn], 8, "adding ability scores brawn") + XCTAssertEqual(abilityScores[reflexes], 10, "adding ability scores reflexes") + XCTAssertEqual(abilityScores[stamina], 17, "adding ability scores stamina") + } + + func testAddingUnrelatedScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + + let intelligence = Ability("Intelligence") + let wisdom = Ability("Wisdom") + let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) + let combinedScores = abilityScores + unrelatedScores + + XCTAssertEqual(combinedScores.count, 3, "adding ability scores count") + + XCTAssertEqual(combinedScores[brawn], 8, "adding ability scores brawn") + XCTAssertEqual(combinedScores[reflexes], 13, "adding ability scores reflexes") + XCTAssertEqual(combinedScores[stamina], 17, "adding ability scores stamina") } func testSubtractingAbilityScores() { - // Test adding scores with modifiers using = ... + ... - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - let combinedScores = abilityScores - abilityScores.modifiers - - XCTAssertEqual(combinedScores[brawn], 9, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 12, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 14, "adding ability scores stamina") - } + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") - // Test adding one score using += ... - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - let oneScore = AbilityScores([reflexes: -3]) - abilityScores -= oneScore - - XCTAssertEqual(abilityScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(abilityScores[reflexes], 16, "adding ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 17, "adding ability scores stamina") - } + let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + let combinedScores = abilityScores - abilityScores.modifiers - // Test that adding unrelated scores has no effect - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) - - let intelligence = Ability("Intelligence") - let wisdom = Ability("Wisdom") - let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) - let combinedScores = abilityScores - unrelatedScores - - XCTAssertEqual(combinedScores.count, 3, "adding ability scores count") - - XCTAssertEqual(combinedScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 13, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 17, "adding ability scores stamina") - } + XCTAssertEqual(combinedScores[brawn], 9, "adding ability scores brawn") + XCTAssertEqual(combinedScores[reflexes], 12, "adding ability scores reflexes") + XCTAssertEqual(combinedScores[stamina], 14, "adding ability scores stamina") + } + + func testSubtractingOneScore() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + var abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + let oneScore = AbilityScores([reflexes: -3]) + abilityScores -= oneScore + + XCTAssertEqual(abilityScores[brawn], 8, "adding ability scores brawn") + XCTAssertEqual(abilityScores[reflexes], 16, "adding ability scores reflexes") + XCTAssertEqual(abilityScores[stamina], 17, "adding ability scores stamina") + } + + func testSubtractingUnrelatedScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) + + let intelligence = Ability("Intelligence") + let wisdom = Ability("Wisdom") + let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) + let combinedScores = abilityScores - unrelatedScores + + XCTAssertEqual(combinedScores.count, 3, "adding ability scores count") + + XCTAssertEqual(combinedScores[brawn], 8, "adding ability scores brawn") + XCTAssertEqual(combinedScores[reflexes], 13, "adding ability scores reflexes") + XCTAssertEqual(combinedScores[stamina], 17, "adding ability scores stamina") } func testDefaultAbilityScores() { - // Test default ability scores - do { - let abilityScores = AbilityScores() - - XCTAssertEqual(abilityScores.count, 6, "default ability scores count") - - // Test names and values - let abilityNames = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"] - for ability in abilityScores.abilities { - XCTAssertTrue(abilityNames.contains(ability.name), "default ability name") - XCTAssertEqual(abilityScores[ability], 0, "default ability score 0") - } - } + let abilityScores = AbilityScores() - // Test default ability scores with non-default abilities - do { - let brawn = Ability("Brawn") - let reflexes = Ability("Reflexes") - let stamina = Ability("Stamina") - - let abilityScores = AbilityScores(defaults: [brawn, reflexes, stamina]) - XCTAssertEqual(abilityScores.count, 3, "default ability scores count") - - // Test values via keys and values - for ability in abilityScores.abilities { - XCTAssertEqual(abilityScores[ability], 0, "default ability score 0") - } - for value in abilityScores.values { - XCTAssertEqual(value, 0, "default ability score 0") - } + XCTAssertEqual(abilityScores.count, 6, "default ability scores count") + + // Test names and values + let abilityNames = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"] + for ability in abilityScores.abilities { + XCTAssertTrue(abilityNames.contains(ability.name), "default ability name") + XCTAssertEqual(abilityScores[ability], 0, "default ability score 0") } } + func testNonDefaultAbilityScores() { + let brawn = Ability("Brawn") + let reflexes = Ability("Reflexes") + let stamina = Ability("Stamina") + + let abilityScores = AbilityScores(defaults: [brawn, reflexes, stamina]) + XCTAssertEqual(abilityScores.count, 3, "default ability scores count") + + // Test values via keys and values + for ability in abilityScores.abilities { + XCTAssertEqual(abilityScores[ability], 0, "default ability score 0") + } + for value in abilityScores.values { + XCTAssertEqual(value, 0, "default ability score 0") + } + } } diff --git a/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift b/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift index db90c75..4f06172 100644 --- a/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift @@ -14,164 +14,101 @@ import RolePlayingCore class AlignmentTests: XCTestCase { - func testEthics() { - // Test the literal values - do { - XCTAssertEqual(Ethics.lawful.rawValue, "Lawful", "lawful string") - XCTAssertEqual(Ethics.neutral.rawValue, "Neutral", "neutral string") - XCTAssertEqual(Ethics.chaotic.rawValue, "Chaotic", "chaotic string") - - XCTAssertEqual(Ethics.lawful.value, 1.0, "lawful value") - XCTAssertEqual(Ethics.neutral.value, 0.0, "neutral value") - XCTAssertEqual(Ethics.chaotic.value, -1.0, "chaotic value") - } + func testEthicsByString() { + let lawful = Ethics(rawValue: "Lawful") + XCTAssertNotNil(lawful, "lawful should be non-nil") + XCTAssertEqual(lawful, Ethics.lawful, "lawful enum") + XCTAssertEqual(lawful?.value, 1.0, "lawful value") - // Test creation by raw value - do { - let lawful = Ethics(rawValue: "Lawful") - XCTAssertNotNil(lawful, "lawful should be non-nil") - XCTAssertEqual(lawful, Ethics.lawful, "lawful enum") - XCTAssertEqual(lawful?.value, 1.0, "lawful value") - - let neutral = Ethics(rawValue: "Neutral") - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Ethics.neutral, "neutral enum") - XCTAssertEqual(neutral?.value, 0.0, "neutral value") - - let chaotic = Ethics(rawValue: "Chaotic") - XCTAssertNotNil(chaotic, "chaotic should be non-nil") - XCTAssertEqual(chaotic, Ethics.chaotic, "chaotic enum") - XCTAssertEqual(chaotic?.value, -1.0, "chaotic value") - } + let neutral = Ethics(rawValue: "Neutral") + XCTAssertNotNil(neutral, "neutral should be non-nil") + XCTAssertEqual(neutral, Ethics.neutral, "neutral enum") + XCTAssertEqual(neutral?.value, 0.0, "neutral value") - // Test creation by Double value - do { - let lawful = Ethics(0.8) - XCTAssertNotNil(lawful, "lawful should be non-nil") - XCTAssertEqual(lawful, Ethics.lawful, "lawful enum") - XCTAssertEqual(lawful.value, 1.0, "lawful value") - - let neutral = Ethics(-0.1) - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Ethics.neutral, "neutral enum") - XCTAssertEqual(neutral.value, 0.0, "neutral value") - - let chaotic = Ethics(-0.34) - XCTAssertNotNil(chaotic, "chaotic should be non-nil") - XCTAssertEqual(chaotic, Ethics.chaotic, "chaotic enum") - XCTAssertEqual(chaotic.value, -1.0, "chaotic value") - } + let chaotic = Ethics(rawValue: "Chaotic") + XCTAssertNotNil(chaotic, "chaotic should be non-nil") + XCTAssertEqual(chaotic, Ethics.chaotic, "chaotic enum") + XCTAssertEqual(chaotic?.value, -1.0, "chaotic value") } - func testMorals() { - // Test the literal values - do { - XCTAssertEqual(Morals.good.rawValue, "Good", "good string") - XCTAssertEqual(Morals.neutral.rawValue, "Neutral", "neutral string") - XCTAssertEqual(Morals.evil.rawValue, "Evil", "evil string") - - XCTAssertEqual(Morals.good.value, 1.0, "good value") - XCTAssertEqual(Morals.neutral.value, 0.0, "neutral value") - XCTAssertEqual(Morals.evil.value, -1.0, "evil value") - } + func testEthicsByValue() { + let lawful = Ethics(0.8) + XCTAssertNotNil(lawful, "lawful should be non-nil") + XCTAssertEqual(lawful, Ethics.lawful, "lawful enum") + XCTAssertEqual(lawful.value, 1.0, "lawful value") - // Test creation by raw value - do { - let good = Morals(rawValue: "Good") - XCTAssertNotNil(good, "good should be non-nil") - XCTAssertEqual(good, Morals.good, "good enum") - XCTAssertEqual(good?.value, 1.0, "good value") - - let neutral = Morals(rawValue: "Neutral") - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Morals.neutral, "neutral enum") - XCTAssertEqual(neutral?.value, 0.0, "neutral value") - - let evil = Morals(rawValue: "Evil") - XCTAssertNotNil(evil, "evil should be non-nil") - XCTAssertEqual(evil, Morals.evil, "evil enum") - XCTAssertEqual(evil?.value, -1.0, "evil value") - } + let neutral = Ethics(-0.1) + XCTAssertNotNil(neutral, "neutral should be non-nil") + XCTAssertEqual(neutral, Ethics.neutral, "neutral enum") + XCTAssertEqual(neutral.value, 0.0, "neutral value") - // Test creation by Double value - do { - let good = Morals(0.334) - XCTAssertNotNil(good, "good should be non-nil") - XCTAssertEqual(good, Morals.good, "good enum") - XCTAssertEqual(good.value, 1.0, "good value") - - let neutral = Morals(0.2) - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Morals.neutral, "neutral enum") - XCTAssertEqual(neutral.value, 0.0, "neutral value") - - let evil = Morals(-0.9) - XCTAssertNotNil(evil, "evil should be non-nil") - XCTAssertEqual(evil, Morals.evil, "evil enum") - XCTAssertEqual(evil.value, -1.0, "evil value") - } - + let chaotic = Ethics(-0.34) + XCTAssertNotNil(chaotic, "chaotic should be non-nil") + XCTAssertEqual(chaotic, Ethics.chaotic, "chaotic enum") + XCTAssertEqual(chaotic.value, -1.0, "chaotic value") } - func testAlignmentType() { - // Test creation by raw value - do { - let neutralGood = Alignment.Kind(.neutral, .good) - XCTAssertEqual(neutralGood.description, "Neutral Good", "description") - - let neutralEvil = Alignment.Kind(.neutral, .evil) - XCTAssertEqual(neutralEvil.description, "Neutral Evil", "description") - - let chaoticNeutral = Alignment.Kind(.chaotic, .neutral) - XCTAssertEqual(chaoticNeutral.description, "Chaotic Neutral", "description") - - XCTAssertEqual(neutralGood, Alignment.Kind(.neutral, .good), "equatable") - XCTAssertNotEqual(neutralGood, neutralEvil, "equatable") - XCTAssertNotEqual(neutralEvil, chaoticNeutral, "equatable") - } + func testMoralsByString() { + let good = Morals(rawValue: "Good") + XCTAssertNotNil(good, "good should be non-nil") + XCTAssertEqual(good, Morals.good, "good enum") + XCTAssertEqual(good?.value, 1.0, "good value") - // Test creation by Double value - do { - let lawfulNeutral = Alignment(ethics: 0.7, morals: 0.0) - XCTAssertEqual(lawfulNeutral.description, "Lawful Neutral", "description") - - let neutral = Alignment(ethics: 0.1, morals: -0.2) - XCTAssertEqual(neutral.description, "Neutral", "description") - - let chaoticGood = Alignment(ethics: -1, morals: 1) - XCTAssertEqual(chaoticGood.description, "Chaotic Good", "description") - } + let neutral = Morals(rawValue: "Neutral") + XCTAssertNotNil(neutral, "neutral should be non-nil") + XCTAssertEqual(neutral, Morals.neutral, "neutral enum") + XCTAssertEqual(neutral?.value, 0.0, "neutral value") + + let evil = Morals(rawValue: "Evil") + XCTAssertNotNil(evil, "evil should be non-nil") + XCTAssertEqual(evil, Morals.evil, "evil enum") + XCTAssertEqual(evil?.value, -1.0, "evil value") + } + + func testMoralsByValue() { + let good = Morals(0.334) + XCTAssertNotNil(good, "good should be non-nil") + XCTAssertEqual(good, Morals.good, "good enum") + XCTAssertEqual(good.value, 1.0, "good value") + + let neutral = Morals(0.2) + XCTAssertNotNil(neutral, "neutral should be non-nil") + XCTAssertEqual(neutral, Morals.neutral, "neutral enum") + XCTAssertEqual(neutral.value, 0.0, "neutral value") + + let evil = Morals(-0.9) + XCTAssertNotNil(evil, "evil should be non-nil") + XCTAssertEqual(evil, Morals.evil, "evil enum") + XCTAssertEqual(evil.value, -1.0, "evil value") } - func testAlignment() { - // Test creation by raw value - do { - let lawfulGood = Alignment(.lawful, .good) - XCTAssertEqual(lawfulGood.description, "Lawful Good", "description") - - let neutralGood = Alignment(.neutral, .good) - XCTAssertEqual(neutralGood.description, "Neutral Good", "description") - - let chaoticNeutral = Alignment(.chaotic, .neutral) - XCTAssertEqual(chaoticNeutral.description, "Chaotic Neutral", "description") - - XCTAssertEqual(lawfulGood, Alignment(.lawful, .good), "equatable") - XCTAssertNotEqual(lawfulGood, neutralGood, "equatable") - XCTAssertNotEqual(neutralGood, chaoticNeutral, "equatable") - } + func testAlignmentByType() { + let neutralGood = Alignment.Kind(.neutral, .good) + XCTAssertEqual(neutralGood.description, "Neutral Good", "description") - // Test creation by Double value - do { - let lawfulNeutral = Alignment(ethics: 0.9, morals: 0.0) - XCTAssertEqual(lawfulNeutral.description, "Lawful Neutral", "description") - - let neutral = Alignment(ethics: 0.1, morals: -0.1) - XCTAssertEqual(neutral.description, "Neutral", "description") - - let chaoticEvil = Alignment(ethics: -1, morals: -1) - XCTAssertEqual(chaoticEvil.description, "Chaotic Evil", "description") - } + let neutralEvil = Alignment.Kind(.neutral, .evil) + XCTAssertEqual(neutralEvil.description, "Neutral Evil", "description") + + let chaoticNeutral = Alignment.Kind(.chaotic, .neutral) + XCTAssertEqual(chaoticNeutral.description, "Chaotic Neutral", "description") + XCTAssertEqual(neutralGood, Alignment.Kind(.neutral, .good), "equatable") + XCTAssertNotEqual(neutralGood, neutralEvil, "equatable") + XCTAssertNotEqual(neutralEvil, chaoticNeutral, "equatable") + } + + func testAlignmentByValue() { + let lawfulNeutral = Alignment(ethics: 0.7, morals: 0.0) + XCTAssertEqual(lawfulNeutral.description, "Lawful Neutral", "description") + + let neutral = Alignment(ethics: 0.1, morals: -0.2) + XCTAssertEqual(neutral.description, "Neutral", "description") + + let chaoticGood = Alignment(ethics: -1, morals: 1) + XCTAssertEqual(chaoticGood.description, "Chaotic Good", "description") + } + + func testCHangingAlignment() { // Test changing alignment do { var alignment = Alignment(.neutral, .evil) @@ -207,43 +144,36 @@ class AlignmentTests: XCTestCase { func testAlignmentParsing() { // Test initializing from valid string - do { - let neutralGood = "Neutral Good".parseAlignment - XCTAssertNotNil(neutralGood, "alignment should be non-nil") - XCTAssertEqual(neutralGood?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(neutralGood?.1, Morals.good, "morals enumeration") - - let neutral = "Neutral".parseAlignment - XCTAssertNotNil(neutral, "alignment should be non-nil") - XCTAssertEqual(neutral?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(neutral?.1, Morals.neutral, "morals enumeration") - } + let neutralGood = "Neutral Good".parseAlignment + XCTAssertNotNil(neutralGood, "alignment should be non-nil") + XCTAssertEqual(neutralGood?.0, Ethics.neutral, "ethics enumeration") + XCTAssertEqual(neutralGood?.1, Morals.good, "morals enumeration") + let neutral = "Neutral".parseAlignment + XCTAssertNotNil(neutral, "alignment should be non-nil") + XCTAssertEqual(neutral?.0, Ethics.neutral, "ethics enumeration") + XCTAssertEqual(neutral?.1, Morals.neutral, "morals enumeration") + // Test initializing from partial valid string - do { - let chaotic = "Chaotic".parseAlignment - XCTAssertNotNil(chaotic, "alignment should be non-nil") - XCTAssertEqual(chaotic?.0, Ethics.chaotic, "ethics enumeration") - XCTAssertEqual(chaotic?.1, Morals.neutral, "morals enumeration") - - let good = "Good".parseAlignment - XCTAssertNotNil(good, "alignment should be non-nil") - XCTAssertEqual(good?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(good?.1, Morals.good, "morals enumeration") - } + let chaotic = "Chaotic".parseAlignment + XCTAssertNotNil(chaotic, "alignment should be non-nil") + XCTAssertEqual(chaotic?.0, Ethics.chaotic, "ethics enumeration") + XCTAssertEqual(chaotic?.1, Morals.neutral, "morals enumeration") + let good = "Good".parseAlignment + XCTAssertNotNil(good, "alignment should be non-nil") + XCTAssertEqual(good?.0, Ethics.neutral, "ethics enumeration") + XCTAssertEqual(good?.1, Morals.good, "morals enumeration") + // Test initializing from bad string - do { - let tooManyWords = "Neutral Neutral Neutral".parseAlignment - XCTAssertNil(tooManyWords, "too many words should be nil") - - let mismatchedWords = "Foo Bar".parseAlignment - XCTAssertNil(mismatchedWords, "mismatched words should be nil") - - let wrongWord = "Cat".parseAlignment - XCTAssertNil(wrongWord, "wrong word should be nil") - } + let tooManyWords = "Neutral Neutral Neutral".parseAlignment + XCTAssertNil(tooManyWords, "too many words should be nil") + let mismatchedWords = "Foo Bar".parseAlignment + XCTAssertNil(mismatchedWords, "mismatched words should be nil") + + let wrongWord = "Cat".parseAlignment + XCTAssertNil(wrongWord, "wrong word should be nil") } func testAlignmentDictionaryDecoding() { @@ -320,88 +250,78 @@ class AlignmentTests: XCTestCase { } // Test string values - do { - let stringTrait = """ - { - "alignment": "Chaotic Evil" - } - """.data(using: .utf8)! - do { - let decoded = try decoder.decode(AlignmentContainer.self, from: stringTrait) - XCTAssertEqual("\(decoded.alignment)", "Chaotic Evil", "decoded string should be round-trip") + let stringTrait = """ + { + "alignment": "Chaotic Evil" } - catch let error { - XCTFail("Error decoding string trait: \(error)") - } - - let notValidTrait = """ - { - "alignment": "Hello" - } - """.data(using: .utf8)! - do { - let notValid = try decoder.decode(AlignmentContainer.self, from: notValidTrait) - XCTAssertNil(notValid, "non-valid traits should be nil") - } - catch let error { - print("Successfully failed decoding invalid string trait: \(error)") + """.data(using: .utf8)! + do { + let decoded = try decoder.decode(AlignmentContainer.self, from: stringTrait) + XCTAssertEqual("\(decoded.alignment)", "Chaotic Evil", "decoded string should be round-trip") + } + catch let error { + XCTFail("Error decoding string trait: \(error)") + } + + let notValidTrait = """ + { + "alignment": "Hello" } + """.data(using: .utf8)! + do { + let notValid = try decoder.decode(AlignmentContainer.self, from: notValidTrait) + XCTAssertNil(notValid, "non-valid traits should be nil") + } + catch let error { + print("Successfully failed decoding invalid string trait: \(error)") } } func testAlignmentEncoding() { let encoder = JSONEncoder() - // Test default encoding - do { - struct AlignmentContainer: Encodable { - let alignment: Alignment - } - let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) - let encoded = try encoder.encode(container) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) - - if let dictionary = deserialized as? [String: Any] { - XCTAssertNotNil(dictionary["alignment"] as? [String: Double], "player traits round trip alignment") - if let alignment = dictionary["alignment"] as? [String: Double] { - XCTAssertEqual(alignment["ethics"], -1, "player traits round trip alignment ethics") - XCTAssertEqual(alignment["morals"], 1, "player traits round trip alignment ethics") - } - } - } - catch let error { - XCTFail("Encoding Alignment failed, error: \(error)") + struct AlignmentContainer: Encodable { + let alignment: Alignment } + let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) + let encoded = try! encoder.encode(container) + let deserialized = try? JSONSerialization.jsonObject(with: encoded, options: []) - // Test stringified encoding - do { - struct AlignmentContainer: Encodable { - let alignment: Alignment - - enum CodingKeys: String, CodingKey { - case alignment - } - - // Stringify alignment - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode("\(alignment)", forKey: .alignment) - } + if let dictionary = deserialized as? [String: Any] { + XCTAssertNotNil(dictionary["alignment"] as? [String: Double], "player traits round trip alignment") + if let alignment = dictionary["alignment"] as? [String: Double] { + XCTAssertEqual(alignment["ethics"], -1, "player traits round trip alignment ethics") + XCTAssertEqual(alignment["morals"], 1, "player traits round trip alignment ethics") } + } + } + + func testStringifiedEncoding() { + let encoder = JSONEncoder() + + struct AlignmentContainer: Encodable { + let alignment: Alignment - let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) - let encoded = try! encoder.encode(container) - let deserialized = try? JSONSerialization.jsonObject(with: encoded, options: []) - XCTAssertNotNil(deserialized, "player traits round trip") + enum CodingKeys: String, CodingKey { + case alignment + } - if let dictionary = deserialized as? [String: String] { - if let alignment = dictionary["alignment"] { - XCTAssertEqual(alignment, "Chaotic Good", "player traits round trip alignment ethics") - } + // Stringify alignment + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("\(alignment)", forKey: .alignment) } } + let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) + let encoded = try! encoder.encode(container) + let deserialized = try? JSONSerialization.jsonObject(with: encoded, options: []) + XCTAssertNotNil(deserialized, "player traits round trip") + if let dictionary = deserialized as? [String: String] { + if let alignment = dictionary["alignment"] { + XCTAssertEqual(alignment, "Chaotic Good", "player traits round trip alignment ethics") + } + } } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift b/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift index 7cd5bc2..25a2f1f 100644 --- a/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift @@ -51,5 +51,4 @@ class ConfigurationTests: XCTestCase { print("Invalid configuration correctly threw an error: \(error)") } } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/CurrencyTests.swift b/RolePlayingCore/RolePlayingCoreTests/CurrencyTests.swift index 6b305eb..b4a6c4d 100644 --- a/RolePlayingCore/RolePlayingCoreTests/CurrencyTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/CurrencyTests.swift @@ -10,7 +10,6 @@ import XCTest @testable import RolePlayingCore - class UnitCurrencyTests: XCTestCase { static var currencies: Currencies! diff --git a/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift b/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift index 701dc98..6dd9dc1 100644 --- a/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift @@ -498,7 +498,6 @@ class DiceParserTests: XCTestCase { let roll = badFormatString.parseDice XCTAssertNil(roll, "'\(badFormatString)' consecutive dice expressions") } - } func testDecodingDice() { diff --git a/RolePlayingCore/RolePlayingCoreTests/DiceTests.swift b/RolePlayingCore/RolePlayingCoreTests/DiceTests.swift index aeead93..ed873f1 100644 --- a/RolePlayingCore/RolePlayingCoreTests/DiceTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/DiceTests.swift @@ -10,8 +10,6 @@ import XCTest import RolePlayingCore - - /// Use a sample size large enough to hit relatively tight ranges of /// expected mean, min and max values below. let sampleSize = 1024 @@ -216,7 +214,6 @@ class DiceTests: XCTestCase { print(" lastRoll = \"\(lastRoll.description)\"") // TODO: verify that it is actually dropping the highest score. - } } @@ -282,6 +279,5 @@ class DiceTests: XCTestCase { print(" lastRoll = \"\(lastRoll.description)\"") } } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/HeightTests.swift b/RolePlayingCore/RolePlayingCoreTests/HeightTests.swift index 68bf8ce..54496b2 100644 --- a/RolePlayingCore/RolePlayingCoreTests/HeightTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/HeightTests.swift @@ -11,11 +11,8 @@ import XCTest import RolePlayingCore class UnitHeightTests: XCTestCase { - - let decoder = JSONDecoder() func testHeights() { - do { let howTall = "5".parseHeight XCTAssertNotNil(howTall, "height should be non-nil") @@ -58,16 +55,11 @@ class UnitHeightTests: XCTestCase { XCTAssertNotNil(howTall, "height should be non-nil") XCTAssertEqual(howTall?.value, 2.1, "height should be 2.1") } - - } func testInvalidHeights() { - do { - let howTall = "3 hello".parseHeight - XCTAssertNil(howTall, "height should be nil") - } - + let howTall = "3 hello".parseHeight + XCTAssertNil(howTall, "height should be nil") } func testEncodingHeight() { @@ -122,7 +114,6 @@ class UnitHeightTests: XCTestCase { } } - // Test decoding from double height do { let traits = """ @@ -157,7 +148,6 @@ class UnitHeightTests: XCTestCase { print("Decoding invalid height successfully threw an error: \(error)") } } - } func testDecodingHeightIfPresent() { @@ -200,7 +190,6 @@ class UnitHeightTests: XCTestCase { } } } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift index f513955..fbe335c 100644 --- a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift @@ -70,133 +70,130 @@ class PlayerTests: XCTestCase { self.human = try! decoder.decode(SpeciesTraits.self, from: self.humanTraits) } - func testPlayer() { + func testPlayer() { + let player = Player("Frodo", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .female, alignment: Alignment(.lawful, .neutral)) + XCTAssertEqual(player.name, "Frodo", "player name") + XCTAssertEqual(player.className, "Fighter", "class name") + XCTAssertEqual(player.speciesName, "Human", "species name") + + XCTAssertEqual(player.descriptiveTraits.count, 0, "descriptiveTraits") + + XCTAssertEqual(player.gender, Player.Gender.female, "gender") + XCTAssertEqual(player.alignment, Alignment(.lawful, .neutral), "alignment") + + // Abilities is scores plus species modifiers, so + 1 + for key in player.abilities.abilities { + let score = player.abilities[key]! + XCTAssertTrue((4...19).contains(score), "ability score \(score) for \(key)") + } + + // I do the maths + XCTAssertTrue((4..<7).contains(player.height.value), "height \(player.height.value)") + + XCTAssertTrue((1...10).contains(player.maximumHitPoints), "maximum hit points") + XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") + XCTAssertEqual("\(player.classTraits.hitDice)", "d10", "hit dice") + XCTAssertEqual(player.experiencePoints, 0, "experience points") + XCTAssertEqual(player.level, 1, "level") + + XCTAssertTrue((50...200).contains(player.money.value), "money \(player.money.value)") + + XCTAssertEqual(player.proficiencyBonus, 2, "proficiency bonus") + } + + func testMinimumTraitsPlayer() { let decoder = JSONDecoder() - // Test construction from types + let playerTraits = """ + { + "name": "Bilbo", + "background": "Sailor", + "species": "Human", + "class": "Fighter", + "gender": "Male", + "height": "3'9\\"", + "ability scores": {"Dexterity": 13, "Charisma": 12}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skills": ["Athletics"], + "money": 130, + "maximum hit points": 10 + } + """.data(using: .utf8)! + do { - let player = Player("Frodo", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .female, alignment: Alignment(.lawful, .neutral)) - XCTAssertEqual(player.name, "Frodo", "player name") + let player = try decoder.decode(Player.self, from: playerTraits) + player.speciesTraits = human + player.classTraits = fighter + + XCTAssertEqual(player.name, "Bilbo", "player name") XCTAssertEqual(player.className, "Fighter", "class name") XCTAssertEqual(player.speciesName, "Human", "species name") - XCTAssertEqual(player.descriptiveTraits.count, 0, "descriptiveTraits") + XCTAssertEqual(player.gender, Player.Gender.male, "gender") + XCTAssertNil(player.alignment, "alignment") - XCTAssertEqual(player.gender, Player.Gender.female, "gender") - XCTAssertEqual(player.alignment, Alignment(.lawful, .neutral), "alignment") + XCTAssertEqual(player.abilities[.dexterity], 14, "dexterity") + XCTAssertEqual(player.abilities[.charisma], 12, "charisma") - // Abilities is scores plus species modifiers, so + 1 - for key in player.abilities.abilities { - let score = player.abilities[key]! - XCTAssertTrue((4...19).contains(score), "ability score \(score) for \(key)") - } + XCTAssertEqual(player.height.value, 3.75, "height") - // I do the maths - XCTAssertTrue((4..<7).contains(player.height.value), "height \(player.height.value)") - - XCTAssertTrue((1...10).contains(player.maximumHitPoints), "maximum hit points") + XCTAssertEqual(player.maximumHitPoints, 10, "maximum hit points") XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") - XCTAssertEqual("\(player.classTraits.hitDice)", "d10", "hit dice") + XCTAssertEqual(player.experiencePoints, 0, "experience points") XCTAssertEqual(player.level, 1, "level") - XCTAssertTrue((50...200).contains(player.money.value), "money \(player.money.value)") + XCTAssertEqual(player.money.value, 130, "money") - XCTAssertEqual(player.proficiencyBonus, 2, "proficiency bonus") } - - // Test construction from minimum required traits - do { - let playerTraits = """ - { - "name": "Bilbo", - "background": "Sailor", - "species": "Human", - "class": "Fighter", - "gender": "Male", - "height": "3'9\\"", - "ability scores": {"Dexterity": 13, "Charisma": 12}, - "background ability scores": ["Strength", "Strength", "Dexterity"], - "skills": ["Athletics"], - "money": 130, - "maximum hit points": 10 - } - """.data(using: .utf8)! - - do { - let player = try decoder.decode(Player.self, from: playerTraits) - player.speciesTraits = human - player.classTraits = fighter - - XCTAssertEqual(player.name, "Bilbo", "player name") - XCTAssertEqual(player.className, "Fighter", "class name") - XCTAssertEqual(player.speciesName, "Human", "species name") - - XCTAssertEqual(player.gender, Player.Gender.male, "gender") - XCTAssertNil(player.alignment, "alignment") - - XCTAssertEqual(player.abilities[.dexterity], 14, "dexterity") - XCTAssertEqual(player.abilities[.charisma], 12, "charisma") - - XCTAssertEqual(player.height.value, 3.75, "height") - - XCTAssertEqual(player.maximumHitPoints, 10, "maximum hit points") - XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") - - XCTAssertEqual(player.experiencePoints, 0, "experience points") - XCTAssertEqual(player.level, 1, "level") - - XCTAssertEqual(player.money.value, 130, "money") - - } - catch let error { - XCTFail("decode player failed, error: \(error)") - } + catch let error { + XCTFail("decode player failed, error: \(error)") + } + } + + func testOptionalPlayerTraits() { + let decoder = JSONDecoder() + + let playerTraits = """ + { + "name": "Bilbo", + "background": "Sailor", + "species": "Human", + "class": "Fighter", + "alignment": "Lawful Evil", + "height": "3'9\\"", + "ability scores": {"Strength": 12}, + "background ability scores": ["Strength", "Strength", "Dexterity"], + "skills": ["Athletics"], + "money": 130, + "maximum hit points": 10, + "experience points": 2300, + "level": 2 } + """.data(using: .utf8)! - // Test construction with optional traits do { - let playerTraits = """ - { - "name": "Bilbo", - "background": "Sailor", - "species": "Human", - "class": "Fighter", - "alignment": "Lawful Evil", - "height": "3'9\\"", - "ability scores": {"Strength": 12}, - "background ability scores": ["Strength", "Strength", "Dexterity"], - "skills": ["Athletics"], - "money": 130, - "maximum hit points": 10, - "experience points": 2300, - "level": 2 - } - """.data(using: .utf8)! + let player = try decoder.decode(Player.self, from: playerTraits) + player.speciesTraits = human + player.classTraits = fighter - do { - let player = try decoder.decode(Player.self, from: playerTraits) - player.speciesTraits = human - player.classTraits = fighter - - XCTAssertNil(player.gender, "gender") - XCTAssertEqual(player.alignment, Alignment(.lawful, .evil), "alignment") - - XCTAssertEqual(player.canLevelUp, true, "level up") - XCTAssertEqual("\(player.hitDice)", "2d10", "hit dice") - player.levelUp() - XCTAssertEqual(player.level, 3, "level") - XCTAssertTrue(player.maximumHitPoints > 15, "experience points") - - XCTAssertEqual(player.canLevelUp, false, "level up") - XCTAssertEqual("\(player.hitDice)", "3d10", "hit dice") - - player.levelUp() - XCTAssertEqual(player.level, 3, "level") - } - catch let error { - XCTFail("decode player failed, error: \(error)") - } + XCTAssertNil(player.gender, "gender") + XCTAssertEqual(player.alignment, Alignment(.lawful, .evil), "alignment") + + XCTAssertEqual(player.canLevelUp, true, "level up") + XCTAssertEqual("\(player.hitDice)", "2d10", "hit dice") + player.levelUp() + XCTAssertEqual(player.level, 3, "level") + XCTAssertTrue(player.maximumHitPoints > 15, "experience points") + + XCTAssertEqual(player.canLevelUp, false, "level up") + XCTAssertEqual("\(player.hitDice)", "3d10", "hit dice") + + player.levelUp() + XCTAssertEqual(player.level, 3, "level") + } + catch let error { + XCTFail("decode player failed, error: \(error)") } } @@ -328,7 +325,7 @@ class PlayerTests: XCTestCase { func expectedModifier(for abilityScore: Int) -> Int { let selfMinus10 = abilityScore - 10 - return selfMinus10 < 0 ? Int(floor(Double(selfMinus10) / 2.0)) : selfMinus10 / 2 + return selfMinus10 < 0 ? Int((Double(selfMinus10) / 2.0).rounded(.down)) : selfMinus10 / 2 } func testComputedProperties() { @@ -639,5 +636,4 @@ class PlayerTests: XCTestCase { player.levelUp() // Should do nothing XCTAssertEqual(player.level, 4) } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/RolePlayingCoreTests.swift b/RolePlayingCore/RolePlayingCoreTests/RolePlayingCoreTests.swift deleted file mode 100644 index e091ad9..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/RolePlayingCoreTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// RolePlayingCoreTests.swift -// RolePlayingCoreTests -// -// Created by Brian Arnold on 1/1/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -import RolePlayingCore - -class RolePlayingCoreTests: XCTestCase { - - - func testRolePlayingCore() { - // TODO: implement an integration test for the whole library. - } - -} diff --git a/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift b/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift index 75bd7e8..3341912 100644 --- a/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift @@ -24,7 +24,5 @@ class ServiceErrorTests: XCTestCase { XCTAssertTrue(description.contains("testServiceError"), "should have throw function name in it") XCTAssertTrue(description.contains("ServiceErrorTests"), "should have throw file name in it") } - } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift index 7a41677..214dd65 100644 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift @@ -57,5 +57,4 @@ class SpeciesTests: XCTestCase { XCTFail("Species threw an error: \(error)") } } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift index 00f7905..b7aa70f 100644 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift @@ -15,90 +15,79 @@ class SpeciesTraitsTests: XCTestCase { let decoder = JSONDecoder() func testSpeciesTraits() { - // Test typical traits - do { - let traits = """ - { - "name": "Human", - "plural": "Humans", - "lifespan": 90, - "speed": 30, - "languages": ["Common"], - "extra languages": 1 - } - """.data(using: .utf8)! - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") + let traits = """ + { + "name": "Human", + "plural": "Humans", + "lifespan": 90, + "speed": 30, + "languages": ["Common"], + "extra languages": 1 } - - XCTAssertNotNil(speciesTraits) - XCTAssertEqual(speciesTraits?.name, "Human", "name") - XCTAssertEqual(speciesTraits?.plural, "Humans", "plural") - XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") - - XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") - - XCTAssertEqual(speciesTraits?.speed, 30, "speed") - } - - // Test minimum traits + """.data(using: .utf8)! + var speciesTraits: SpeciesTraits? = nil do { - let traits = """ - { - "name": "Giant Human", - "plural": "Giant Humans", - "lifespan": 90, - "speed": 30 - } - """.data(using: .utf8)! - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") - } - XCTAssertNotNil(speciesTraits) - XCTAssertEqual(speciesTraits?.name, "Giant Human", "name") - XCTAssertEqual(speciesTraits?.plural, "Giant Humans", "plural") - - XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") - - XCTAssertEqual(speciesTraits?.speed, 30, "speed") - - XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") + speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + } + catch let error { + XCTFail("Failed to decode species traits, error: \(error)") } - // Test optional traits - do { - let traits = """ - { - "name": "Small Human", - "plural": "Small Humans", - "lifespan": 90, - "speed": 30, - "aliases": ["Big Human"] - } - """.data(using: .utf8)! - - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + XCTAssertNotNil(speciesTraits) + XCTAssertEqual(speciesTraits?.name, "Human", "name") + XCTAssertEqual(speciesTraits?.plural, "Humans", "plural") + XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") + XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") + XCTAssertEqual(speciesTraits?.speed, 30, "speed") + } + + func testMinimumTraits() { + let traits = """ + { + "name": "Giant Human", + "plural": "Giant Humans", + "lifespan": 90, + "speed": 30 } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") + """.data(using: .utf8)! + var speciesTraits: SpeciesTraits? = nil + do { + speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + } + catch let error { + XCTFail("Failed to decode species traits, error: \(error)") + } + XCTAssertNotNil(speciesTraits) + XCTAssertEqual(speciesTraits?.name, "Giant Human", "name") + XCTAssertEqual(speciesTraits?.plural, "Giant Humans", "plural") + XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") + XCTAssertEqual(speciesTraits?.speed, 30, "speed") + XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") + } + + func testOptionalTraits() { + let traits = """ + { + "name": "Small Human", + "plural": "Small Humans", + "lifespan": 90, + "speed": 30, + "aliases": ["Big Human"] } - XCTAssertNotNil(speciesTraits) - XCTAssertEqual(speciesTraits?.aliases.count, 1, "aliases count") + """.data(using: .utf8)! + + var speciesTraits: SpeciesTraits? = nil + do { + speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + } + catch let error { + XCTFail("Failed to decode species traits, error: \(error)") } + XCTAssertNotNil(speciesTraits) + XCTAssertEqual(speciesTraits?.aliases.count, 1, "aliases count") } func testMissingTraits() { - // Test that each missing trait results in nil do { let traits = "{}".data(using: .utf8)! @@ -114,7 +103,6 @@ class SpeciesTraitsTests: XCTestCase { XCTAssertNil(speciesTraits) } - do { let traits = """ { @@ -146,14 +134,10 @@ class SpeciesTraitsTests: XCTestCase { """.data(using: .utf8)! let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) if let subspeciesTraits = speciesTraits.subspecies.first { - XCTAssertEqual(subspeciesTraits.name, "Subhuman", "name") XCTAssertEqual(subspeciesTraits.plural, "Subhumans", "plural") - XCTAssertEqual(subspeciesTraits.lifespan, 60, "lifespan") - XCTAssertEqual(subspeciesTraits.speed, 10, "speed") - XCTAssertEqual(subspeciesTraits.aliases.count, 0, "aliases") } else { XCTFail("decode failed for traits with subspecies traits") @@ -184,17 +168,12 @@ class SpeciesTraitsTests: XCTestCase { let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) if let subspeciesTraits = speciesTraits.subspecies.first { - XCTAssertNotNil(subspeciesTraits) XCTAssertEqual(subspeciesTraits.name, "Folk", "name") XCTAssertEqual(subspeciesTraits.plural, "Folks", "plural") - XCTAssertEqual(subspeciesTraits.lifespan, 90, "lifespan") - XCTAssertEqual(subspeciesTraits.speed, 30, "speed") - XCTAssertEqual(subspeciesTraits.aliases.count, 1, "aliases") - XCTAssertEqual(subspeciesTraits.baseSizes, speciesTraits.baseSizes, "size") } else { XCTFail("decode failed for traits with subspecies traits") @@ -222,9 +201,7 @@ class SpeciesTraitsTests: XCTestCase { // Confirm species traits XCTAssertEqual(dictionary["name"] as? String, "Human", "encoding name") XCTAssertEqual(dictionary["plural"] as? String, "Humans", "encoding name") - XCTAssertEqual(dictionary["lifespan"] as? Int, 90, "encoding lifespan") - XCTAssertEqual(dictionary["darkvision"] as? Int, 0, "encoding name") XCTAssertEqual(dictionary["speed"] as? Int, 45, "encoding name") @@ -232,9 +209,7 @@ class SpeciesTraitsTests: XCTestCase { if let subspecies = dictionary["subspecies"] as? [[String: Any]], let firstSubspecies = subspecies.first { XCTAssertEqual(firstSubspecies["name"] as? String, "Subhuman", "encoding name") XCTAssertEqual(firstSubspecies["plural"] as? String, "Subhumans", "encoding name") - XCTAssertEqual(firstSubspecies["lifespan"] as? Int, 45, "encoding lifespan") - XCTAssertNil(firstSubspecies["darkvision"], "encoding darkvision") XCTAssertEqual(firstSubspecies["speed"] as? Int, 30, "encoding speed") } else { @@ -257,9 +232,7 @@ class SpeciesTraitsTests: XCTestCase { if let subspecies = dictionary["subspecies"] as? [[String: Any]], let firstSubspecies = subspecies.first { XCTAssertEqual(firstSubspecies["name"] as? String, "Subhuman", "encoding name") XCTAssertEqual(firstSubspecies["plural"] as? String, "Subhumans", "encoding name") - XCTAssertEqual(firstSubspecies["lifespan"] as? Int, 45, "encoding lifespan") - XCTAssertEqual(firstSubspecies["darkvision"] as? Int, 10, "encoding darkvision") XCTAssertNil(firstSubspecies["speed"], "encoding speed") } else { @@ -269,7 +242,5 @@ class SpeciesTraitsTests: XCTestCase { catch let error { XCTFail("decode failed with error: \(error)") } - } - } diff --git a/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift b/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift index 49e2109..9fc85b6 100644 --- a/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift @@ -12,8 +12,6 @@ import RolePlayingCore class UnitWeightTests: XCTestCase { - let decoder = JSONDecoder() - func testWeights() { do { let howHeavy = "70".parseWeight @@ -82,7 +80,6 @@ class UnitWeightTests: XCTestCase { let weight: Weight } - // Test decoding from string height do { let traits = """ { @@ -99,8 +96,6 @@ class UnitWeightTests: XCTestCase { } } - - // Test decoding from double height do { let traits = """ { @@ -117,8 +112,6 @@ class UnitWeightTests: XCTestCase { } } - // Test failure to decode - // Test decoding from double height do { let traits = """ { @@ -134,7 +127,6 @@ class UnitWeightTests: XCTestCase { print("Decoding invalid weight successfully threw an error: \(error)") } } - } func testDecodingWeightIfPresent() { @@ -142,7 +134,6 @@ class UnitWeightTests: XCTestCase { let weight: Weight? // The ? will trigger decodeIfPresent in the decoder } - // Test decoding from string height do { let traits = """ { @@ -159,8 +150,6 @@ class UnitWeightTests: XCTestCase { } } - - // Test decoding from double height do { let traits = """ { @@ -177,5 +166,4 @@ class UnitWeightTests: XCTestCase { } } } - } From a3da7d708acf9f6574b46b676d2971b373ccf919 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Wed, 29 Oct 2025 11:32:23 -0400 Subject: [PATCH 04/33] Converted almost all tests to Swift Testing. --- .../RolePlayingCore.xcodeproj/project.pbxproj | 8 +- .../xcschemes/RolePlayingCore.xcscheme | 5 + .../RolePlayingCore/Currency/Currencies.swift | 16 +- .../RolePlayingCore/Player/Skill.swift | 10 +- .../Player/SpeciesTraits.swift | 3 +- .../RolePlayingCoreTests/AbilityTests.swift | 208 ++--- .../RolePlayingCoreTests/AlignmentTests.swift | 298 ++++---- .../BackgroundsTests.swift | 81 +- .../CharacterGeneratorTests.swift | 2 +- .../ClassTraitsTests.swift | 547 ++++++-------- .../RolePlayingCoreTests/ClassesTests.swift | 53 +- .../ConfigurationTests.swift | 58 +- .../RolePlayingCoreTests/CurrencyTests.swift | 326 ++++---- .../DiceParserTests.swift | 709 ++++++++---------- .../RolePlayingCoreTests/DiceTests.swift | 369 ++++----- .../RolePlayingCoreTests/HeightTests.swift | 133 ++-- .../RolePlayingCoreTests/JSONFileTests.swift | 103 ++- .../NameGeneratorTests.swift | 19 +- .../RolePlayingCoreTests/PlayerTests.swift | 535 ++++++------- .../RolePlayingCoreTests/PlayersTests.swift | 102 +-- .../ServiceErrorTests.swift | 22 +- .../SpeciesNamesTests.swift | 84 ++- .../RolePlayingCoreTests/SpeciesTests.swift | 67 +- .../SpeciesTraitsTests.swift | 274 +++---- .../TestBundleClass.swift | 13 + .../RolePlayingCoreTests/TestMoreSpecies.json | 20 - .../TestSpeciesNames.json | 6 - .../RolePlayingCoreTests/WeightTests.swift | 123 ++- 28 files changed, 1852 insertions(+), 2342 deletions(-) create mode 100644 RolePlayingCore/RolePlayingCoreTests/TestBundleClass.swift diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj index 662825d..6e9e845 100644 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj +++ b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ B6CF53E71E5742C700CADD9F /* JSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53E61E5742C700CADD9F /* JSONFile.json */; }; B6CF53E91E57460F00CADD9F /* InvalidJSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53E81E57460F00CADD9F /* InvalidJSONFile.json */; }; B6CF53EB1E574ED400CADD9F /* HalfBakedJSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */; }; + B6D226F82EB25ABF00939968 /* TestBundleClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D226F72EB25AB900939968 /* TestBundleClass.swift */; }; B6D2EB1F26D7B7E900F99B35 /* RandomIndexGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */; }; B6F070361E4F991D00F66918 /* AlignmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070351E4F991D00F66918 /* AlignmentTests.swift */; }; B6F070381E4FBD6500F66918 /* SpeciesTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */; }; @@ -152,6 +153,7 @@ B6CF53E61E5742C700CADD9F /* JSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JSONFile.json; sourceTree = ""; }; B6CF53E81E57460F00CADD9F /* InvalidJSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidJSONFile.json; sourceTree = ""; }; B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HalfBakedJSONFile.json; sourceTree = ""; }; + B6D226F72EB25AB900939968 /* TestBundleClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundleClass.swift; sourceTree = ""; }; B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomIndexGenerator.swift; sourceTree = ""; }; B6F070351E4F991D00F66918 /* AlignmentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlignmentTests.swift; sourceTree = ""; }; B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeciesTraits.swift; sourceTree = ""; }; @@ -227,6 +229,7 @@ B62055FB1E19DD23002494AB /* RolePlayingCoreTests */ = { isa = PBXGroup; children = ( + B6D226F72EB25AB900939968 /* TestBundleClass.swift */, B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */, B6F070351E4F991D00F66918 /* AlignmentTests.swift */, B626FA622EAF9F9200359F01 /* BackgroundsTests.swift */, @@ -328,12 +331,12 @@ B626FA5C2EAE81C600359F01 /* Backgrounds.swift */, B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */, B6CF53CF1E54E2E200CADD9F /* Classes.swift */, + B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */, B6688B502EACF5AE000A83DD /* Initiative.swift */, B69F84681E58B8F700A4D2B0 /* Player.swift */, B69F846C1E58D66900A4D2B0 /* Players.swift */, - B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */, - B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */, B626FA582EAE76AA00359F01 /* Skill.swift */, + B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */, B6CF53D11E54E2EF00CADD9F /* Species.swift */, ); path = Player; @@ -519,6 +522,7 @@ B6F4AA4A1F12CD2A000C72D2 /* CharacterGeneratorTests.swift in Sources */, B69F846F1E59155A00A4D2B0 /* PlayersTests.swift in Sources */, B62D89C41F09A3870095D587 /* DiceParserTests.swift in Sources */, + B6D226F82EB25ABF00939968 /* TestBundleClass.swift in Sources */, B626FA642EAF9FCF00359F01 /* BackgroundsTests.swift in Sources */, B6CF53DB1E56443A00CADD9F /* ClassesTests.swift in Sources */, B621A3981F0C052D00E55236 /* NameGeneratorTests.swift in Sources */, diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme b/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme index 933d100..717f947 100644 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme +++ b/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme @@ -57,6 +57,11 @@ BlueprintName = "RolePlayingCoreTests" ReferencedContainer = "container:RolePlayingCore.xcodeproj"> + + + + diff --git a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift b/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift index 8ee70ec..de3a1b2 100644 --- a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift +++ b/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift @@ -11,16 +11,26 @@ public struct Currencies { /// A map of all currently loaded currencies. internal static var allCurrencies: [String: UnitCurrency] = [:] + /// A lock to protect access to allCurrencies from multiple threads. + private static let lock = NSLock() + /// Returns the unit currency corresponding to this symbol. Returns nil if no symbol matches. public static func find(_ symbol: String) -> UnitCurrency? { + lock.lock() + defer { lock.unlock() } return Currencies.allCurrencies[symbol] } public static func add(_ currency: UnitCurrency) { + lock.lock() + defer { lock.unlock() } allCurrencies[currency.symbol] = currency } public static func setDefault(_ newBaseUnit: UnitCurrency) { + lock.lock() + defer { lock.unlock() } + // Remove the old base unit from all currencies. let oldSymbol = UnitCurrency.baseUnitCurrency.symbol guard oldSymbol != newBaseUnit.symbol else { @@ -97,8 +107,12 @@ extension Currencies: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) +// Currencies.lock.lock() + let allCurrenciesSnapshot = Currencies.allCurrencies.values +// Currencies.lock.unlock() + var currencies = [Currency]() - for unitCurrency in Currencies.allCurrencies.values { + for unitCurrency in allCurrenciesSnapshot { let currency = Currency(unitCurrency) currencies.append(currency) } diff --git a/RolePlayingCore/RolePlayingCore/Player/Skill.swift b/RolePlayingCore/RolePlayingCore/Player/Skill.swift index 7dcecac..04b0cfe 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Skill.swift +++ b/RolePlayingCore/RolePlayingCore/Player/Skill.swift @@ -35,11 +35,9 @@ extension Skill { public static let stealth = Skill(name: "Stealth", ability: .dexterity) public static let survival = Skill(name: "Survival", ability: .wisdom) - public static var all: [Skill] { - [ - .acrobatics, .animalHandling, .arcana, .athletics, .deception, .history, .insight, .intimidation, .investigation, .medicine, .nature, .perception, .performance, .persuasion, .religion, .sleightOfHand, .stealth, .survival - ] - } + public static var all: [Skill] = [ + .acrobatics, .animalHandling, .arcana, .athletics, .deception, .history, .insight, .intimidation, .investigation, .medicine, .nature, .perception, .performance, .persuasion, .religion, .sleightOfHand, .stealth, .survival + ] public static func skills(from names: [String]) -> [Skill] { // Use the full set of skills if the names are empty. @@ -49,7 +47,7 @@ extension Skill { } extension Sequence where Element == Skill { - + /// Returns a random array of skills with the specified skill count. public func randomSkills(count: Int) -> [Element] { var selected: [Element] = [] diff --git a/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift b/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift index 23e5748..0c884e5 100644 --- a/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift +++ b/RolePlayingCore/RolePlayingCore/Player/SpeciesTraits.swift @@ -51,8 +51,7 @@ public struct SpeciesTraits { lifespan: Int, baseSizes: [String] = ["4-7"], darkVision: Int, - speed: Int, - hitPointBonus: Int = 0) { + speed: Int) { self.name = name self.plural = plural self.aliases = aliases diff --git a/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift b/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift index b819ed2..c42c155 100644 --- a/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/AbilityTests.swift @@ -6,72 +6,77 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest +import Testing @testable import RolePlayingCore -class AbilityTests: XCTestCase { +@Suite("Ability Tests") +struct AbilityTests { - func testStringAbbreviation() { + @Test("String abbreviation") + func stringAbbreviation() { let strength = "Strength" - XCTAssertEqual(strength.abbreviated, "STR", "Strength abbreviated") + #expect(strength.abbreviated == "STR", "Strength abbreviated") let lettera = "a" - XCTAssertEqual(lettera.abbreviated, "A", "A abbreviated") + #expect(lettera.abbreviated == "A", "A abbreviated") let empty = "" - XCTAssertEqual(empty.abbreviated, "", "empty abbreviated") + #expect(empty.abbreviated == "", "empty abbreviated") } - func testIntScoreModifier() { + @Test("Int score modifier") + func intScoreModifier() { let expectedScores = [-5, -4, -4, -3, -3, -2, -2, -1, -1, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10] for score in 1...30 { let expectedScore = expectedScores[score - 1] - XCTAssertEqual(score.scoreModifier, expectedScore, "score \(score) modifier") + #expect(score.scoreModifier == expectedScore, "score \(score) modifier") } } - func testAbilityStruct() { + @Test("Ability struct") + func abilityStruct() { let strength = Ability("Strength") - XCTAssertEqual(strength.name, "Strength", "strength name") - XCTAssertEqual(strength.abbreviated, "STR", "strength name abbreviated") + #expect(strength.name == "Strength", "strength name") + #expect(strength.abbreviated == "STR", "strength name abbreviated") } - func testAbilityEquatable() { + @Test("Ability equatable") + func abilityEquatable() { let strength = Ability("Strength") - XCTAssertTrue(strength == Ability("Strength"), "strength equatable") + #expect(strength == Ability("Strength"), "strength equatable") + #expect(strength != Ability("Intelligence"), "intelligence not equal to strength") } - func testAbilityHashable() { + @Test("Ability hashable") + func abilityHashable() { let strength = Ability("Strength") let strengthClone = Ability("Strength") let intelligence = Ability("Intelligence") var map: [Ability: Int] = [strength: 12, intelligence: 3] map[strengthClone] = 9 - XCTAssertEqual(map[strength], 9, "strength hashable") + #expect(map[strength] == 9, "strength hashable") } - func testAbilityEncodable() { + @Test("Ability encodable") + func abilityEncodable() throws { struct AbilityContainer: Codable { let ability: Ability } let abilityScores = AbilityContainer(ability: Ability("Strength")) let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(abilityScores) - let dictionary = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) as? [String: String] - XCTAssertNotNil(dictionary, "serialized strength should be non-nil") - let strength = dictionary?["ability"] - XCTAssertEqual(strength, "Strength", "Round-trip for Ability should be Strength") - } - catch let error { - XCTFail("encoding threw an error: \(error)") - } + + let encoded = try encoder.encode(abilityScores) + let dictionary = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) as? [String: String] + let unwrappedDictionary = try #require(dictionary, "serialized strength should be non-nil") + let strength = unwrappedDictionary["ability"] + #expect(strength == "Strength", "Round-trip for Ability should be Strength") } - func testAbilityDecodable() { + @Test("Ability decodable") + func abilityDecodable() throws { let traits = """ { "ability": "Strength" @@ -82,32 +87,29 @@ class AbilityTests: XCTestCase { } let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(AbilityContainer.self, from: traits) - XCTAssertEqual(decoded.ability.name, "Strength", "decoded ability name") - } - catch let error { - XCTFail("decoding threw an error: \(error)") - } + let decoded = try decoder.decode(AbilityContainer.self, from: traits) + #expect(decoded.ability.name == "Strength", "decoded ability name") } - func testNonMutableAbilityScores() { + @Test("Non-mutable ability scores") + func nonMutableAbilityScores() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") let abilityScores = AbilityScores([brawn: 9, reflexes: 12, stamina: 15]) - XCTAssertEqual(abilityScores.count, 3, "ability scores count") - XCTAssertEqual(abilityScores[brawn], 9, "ability scores brawn") - XCTAssertEqual(abilityScores[reflexes], 12, "ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 15, "ability scores stamina") + #expect(abilityScores.count == 3, "ability scores count") + #expect(abilityScores[brawn] == 9, "ability scores brawn") + #expect(abilityScores[reflexes] == 12, "ability scores reflexes") + #expect(abilityScores[stamina] == 15, "ability scores stamina") let expectedModifiers = AbilityScores([brawn: -1, reflexes: 1, stamina: 2]) - XCTAssertEqual(abilityScores.modifiers, expectedModifiers, "ability scores modifiers") + #expect(abilityScores.modifiers == expectedModifiers, "ability scores modifiers") } - func testMutableAbilityScores() { + @Test("Mutable ability scores") + func mutableAbilityScores() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") @@ -119,24 +121,25 @@ class AbilityTests: XCTestCase { abilityScores[stamina] = 18 // Verify that 2 of the 3 scores changed - XCTAssertEqual(abilityScores[brawn], 8, "ability scores reflexes") - XCTAssertEqual(abilityScores[reflexes], 11, "ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 18, "ability scores reflexes") + #expect(abilityScores[brawn] == 8, "ability scores reflexes") + #expect(abilityScores[reflexes] == 11, "ability scores reflexes") + #expect(abilityScores[stamina] == 18, "ability scores reflexes") // Check that modifiers reflect mutated scores let expectedModifiers = AbilityScores([brawn: -1, reflexes: 0, stamina: 4]) - XCTAssertEqual(abilityScores.modifiers, expectedModifiers, "ability scores modifiers") + #expect(abilityScores.modifiers == expectedModifiers, "ability scores modifiers") // Verify that a score can't be nil'd out abilityScores[stamina] = nil - XCTAssertEqual(abilityScores[stamina], 18, "ability scores can't be set to nil") + #expect(abilityScores[stamina] == 18, "ability scores can't be set to nil") let invalidAbility = Ability("Charm") abilityScores[invalidAbility] = 8 - XCTAssertNil(abilityScores[invalidAbility], "invalid ability should not set a score") + #expect(abilityScores[invalidAbility] == nil, "invalid ability should not set a score") } - func testAbilityScoresDecodable() { + @Test("Ability scores decodable") + func abilityScoresDecodable() throws { // Test with implicit [String: Int] as from JSON let traits = """ {"Strength": 12, "Intelligence": 8} @@ -144,42 +147,40 @@ class AbilityTests: XCTestCase { let decoder = JSONDecoder() let abilityScores = try? decoder.decode(AbilityScores.self, from: traits) - XCTAssertNotNil(abilityScores, "ability scores should be non-nil") + let unwrappedAbilityScores = try #require(abilityScores, "ability scores should be non-nil") let strength = Ability("Strength") let intelligence = Ability("Intelligence") - XCTAssertEqual(abilityScores?[strength], 12, "ability scores dictionary strength") - XCTAssertEqual(abilityScores?[intelligence], 8, "ability scores dictionary intelligence") + #expect(unwrappedAbilityScores[strength] == 12, "ability scores dictionary strength") + #expect(unwrappedAbilityScores[intelligence] == 8, "ability scores dictionary intelligence") } - func testAbilityScoresEncodable() { + @Test("Ability scores encodable") + func abilityScoresEncodable() throws { let abilityScores = AbilityScores([Ability("Brawn"): 12, Ability("Charm"): 3]) let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(abilityScores) - let serialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String:Int] - let brawn = serialized?["Brawn"] - let charm = serialized?["Charm"] - XCTAssertEqual(brawn, 12, "encoded brawn") - XCTAssertEqual(charm, 3, "encoded charm") - } - catch let error { - XCTFail("decoding threw an error: \(error)") - } + let encoded = try encoder.encode(abilityScores) + let serialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String:Int] + let brawn = serialized?["Brawn"] + let charm = serialized?["Charm"] + #expect(brawn == 12, "encoded brawn") + #expect(charm == 3, "encoded charm") } - func testAbilityScoreKey() { + @Test("Ability score key") + func abilityScoreKey() { // Housekeeping: code coverage for AbilityKey let wisdomKey = AbilityScores.AbilityKey(stringValue: "Wisdom")! - XCTAssertNil(wisdomKey.intValue, "AbilityKey does not use intValue") + #expect(wisdomKey.intValue == nil, "AbilityKey does not use intValue") let intKey = AbilityScores.AbilityKey(intValue: 2) - XCTAssertNil(intKey, "AbilityKey does not use intValue") + #expect(intKey == nil, "AbilityKey does not use intValue") } - func testAddingModifiers() { + @Test("Adding modifiers") + func addingModifiers() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") @@ -187,12 +188,13 @@ class AbilityTests: XCTestCase { let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) let combinedScores = abilityScores + abilityScores.modifiers - XCTAssertEqual(combinedScores[brawn], 7, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 14, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 20, "adding ability scores stamina") + #expect(combinedScores[brawn] == 7, "adding ability scores brawn") + #expect(combinedScores[reflexes] == 14, "adding ability scores reflexes") + #expect(combinedScores[stamina] == 20, "adding ability scores stamina") } - func testAddingOneScore() { + @Test("Adding one score") + func addingOneScore() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") @@ -201,12 +203,13 @@ class AbilityTests: XCTestCase { let oneScore = AbilityScores([reflexes: -3]) abilityScores += oneScore - XCTAssertEqual(abilityScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(abilityScores[reflexes], 10, "adding ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 17, "adding ability scores stamina") + #expect(abilityScores[brawn] == 8, "adding ability scores brawn") + #expect(abilityScores[reflexes] == 10, "adding ability scores reflexes") + #expect(abilityScores[stamina] == 17, "adding ability scores stamina") } - func testAddingUnrelatedScores() { + @Test("Adding unrelated scores") + func addingUnrelatedScores() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") @@ -218,14 +221,15 @@ class AbilityTests: XCTestCase { let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) let combinedScores = abilityScores + unrelatedScores - XCTAssertEqual(combinedScores.count, 3, "adding ability scores count") + #expect(combinedScores.count == 3, "adding ability scores count") - XCTAssertEqual(combinedScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 13, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 17, "adding ability scores stamina") + #expect(combinedScores[brawn] == 8, "adding ability scores brawn") + #expect(combinedScores[reflexes] == 13, "adding ability scores reflexes") + #expect(combinedScores[stamina] == 17, "adding ability scores stamina") } - func testSubtractingAbilityScores() { + @Test("Subtracting ability scores") + func subtractingAbilityScores() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") @@ -233,12 +237,13 @@ class AbilityTests: XCTestCase { let abilityScores = AbilityScores([brawn: 8, reflexes: 13, stamina: 17]) let combinedScores = abilityScores - abilityScores.modifiers - XCTAssertEqual(combinedScores[brawn], 9, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 12, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 14, "adding ability scores stamina") + #expect(combinedScores[brawn] == 9, "adding ability scores brawn") + #expect(combinedScores[reflexes] == 12, "adding ability scores reflexes") + #expect(combinedScores[stamina] == 14, "adding ability scores stamina") } - func testSubtractingOneScore() { + @Test("Subtracting one score") + func subtractingOneScore() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") @@ -247,12 +252,13 @@ class AbilityTests: XCTestCase { let oneScore = AbilityScores([reflexes: -3]) abilityScores -= oneScore - XCTAssertEqual(abilityScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(abilityScores[reflexes], 16, "adding ability scores reflexes") - XCTAssertEqual(abilityScores[stamina], 17, "adding ability scores stamina") + #expect(abilityScores[brawn] == 8, "adding ability scores brawn") + #expect(abilityScores[reflexes] == 16, "adding ability scores reflexes") + #expect(abilityScores[stamina] == 17, "adding ability scores stamina") } - func testSubtractingUnrelatedScores() { + @Test("Subtracting unrelated scores") + func subtractingUnrelatedScores() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") @@ -264,40 +270,42 @@ class AbilityTests: XCTestCase { let unrelatedScores = AbilityScores([intelligence: 14, wisdom: 5]) let combinedScores = abilityScores - unrelatedScores - XCTAssertEqual(combinedScores.count, 3, "adding ability scores count") + #expect(combinedScores.count == 3, "adding ability scores count") - XCTAssertEqual(combinedScores[brawn], 8, "adding ability scores brawn") - XCTAssertEqual(combinedScores[reflexes], 13, "adding ability scores reflexes") - XCTAssertEqual(combinedScores[stamina], 17, "adding ability scores stamina") + #expect(combinedScores[brawn] == 8, "adding ability scores brawn") + #expect(combinedScores[reflexes] == 13, "adding ability scores reflexes") + #expect(combinedScores[stamina] == 17, "adding ability scores stamina") } - func testDefaultAbilityScores() { + @Test("Default ability scores") + func defaultAbilityScores() { let abilityScores = AbilityScores() - XCTAssertEqual(abilityScores.count, 6, "default ability scores count") + #expect(abilityScores.count == 6, "default ability scores count") // Test names and values let abilityNames = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"] for ability in abilityScores.abilities { - XCTAssertTrue(abilityNames.contains(ability.name), "default ability name") - XCTAssertEqual(abilityScores[ability], 0, "default ability score 0") + #expect(abilityNames.contains(ability.name), "default ability name") + #expect(abilityScores[ability] == 0, "default ability score 0") } } - func testNonDefaultAbilityScores() { + @Test("Non-default ability scores") + func nonDefaultAbilityScores() { let brawn = Ability("Brawn") let reflexes = Ability("Reflexes") let stamina = Ability("Stamina") let abilityScores = AbilityScores(defaults: [brawn, reflexes, stamina]) - XCTAssertEqual(abilityScores.count, 3, "default ability scores count") + #expect(abilityScores.count == 3, "default ability scores count") // Test values via keys and values for ability in abilityScores.abilities { - XCTAssertEqual(abilityScores[ability], 0, "default ability score 0") + #expect(abilityScores[ability] == 0, "default ability score 0") } for value in abilityScores.values { - XCTAssertEqual(value, 0, "default ability score 0") + #expect(value == 0, "default ability score 0") } } } diff --git a/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift b/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift index 4f06172..7084d6f 100644 --- a/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift @@ -7,176 +7,166 @@ // import Foundation - -import XCTest - +import Testing import RolePlayingCore -class AlignmentTests: XCTestCase { +@Suite("Alignment Tests") +struct AlignmentTests { - func testEthicsByString() { - let lawful = Ethics(rawValue: "Lawful") - XCTAssertNotNil(lawful, "lawful should be non-nil") - XCTAssertEqual(lawful, Ethics.lawful, "lawful enum") - XCTAssertEqual(lawful?.value, 1.0, "lawful value") - - let neutral = Ethics(rawValue: "Neutral") - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Ethics.neutral, "neutral enum") - XCTAssertEqual(neutral?.value, 0.0, "neutral value") - - let chaotic = Ethics(rawValue: "Chaotic") - XCTAssertNotNil(chaotic, "chaotic should be non-nil") - XCTAssertEqual(chaotic, Ethics.chaotic, "chaotic enum") - XCTAssertEqual(chaotic?.value, -1.0, "chaotic value") + @Test("Ethics by string") + func ethicsByString() throws { + let lawful = try #require(Ethics(rawValue: "Lawful"), "lawful should be non-nil") + #expect(lawful == Ethics.lawful, "lawful enum") + #expect(lawful.value == 1.0, "lawful value") + + let neutral = try #require(Ethics(rawValue: "Neutral"), "neutral should be non-nil") + #expect(neutral == Ethics.neutral, "neutral enum") + #expect(neutral.value == 0.0, "neutral value") + + let chaotic = try #require(Ethics(rawValue: "Chaotic"), "chaotic should be non-nil") + #expect(chaotic == Ethics.chaotic, "chaotic enum") + #expect(chaotic.value == -1.0, "chaotic value") } - func testEthicsByValue() { + @Test("Ethics by value") + func ethicsByValue() { let lawful = Ethics(0.8) - XCTAssertNotNil(lawful, "lawful should be non-nil") - XCTAssertEqual(lawful, Ethics.lawful, "lawful enum") - XCTAssertEqual(lawful.value, 1.0, "lawful value") + #expect(lawful == Ethics.lawful, "lawful enum") + #expect(lawful.value == 1.0, "lawful value") let neutral = Ethics(-0.1) - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Ethics.neutral, "neutral enum") - XCTAssertEqual(neutral.value, 0.0, "neutral value") + #expect(neutral == Ethics.neutral, "neutral enum") + #expect(neutral.value == 0.0, "neutral value") let chaotic = Ethics(-0.34) - XCTAssertNotNil(chaotic, "chaotic should be non-nil") - XCTAssertEqual(chaotic, Ethics.chaotic, "chaotic enum") - XCTAssertEqual(chaotic.value, -1.0, "chaotic value") + #expect(chaotic == Ethics.chaotic, "chaotic enum") + #expect(chaotic.value == -1.0, "chaotic value") } - func testMoralsByString() { - let good = Morals(rawValue: "Good") - XCTAssertNotNil(good, "good should be non-nil") - XCTAssertEqual(good, Morals.good, "good enum") - XCTAssertEqual(good?.value, 1.0, "good value") - - let neutral = Morals(rawValue: "Neutral") - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Morals.neutral, "neutral enum") - XCTAssertEqual(neutral?.value, 0.0, "neutral value") - - let evil = Morals(rawValue: "Evil") - XCTAssertNotNil(evil, "evil should be non-nil") - XCTAssertEqual(evil, Morals.evil, "evil enum") - XCTAssertEqual(evil?.value, -1.0, "evil value") + @Test("Morals by string") + func moralsByString() throws { + let good = try #require(Morals(rawValue: "Good"), "good should be non-nil") + #expect(good == Morals.good, "good enum") + #expect(good.value == 1.0, "good value") + + let neutral = try #require(Morals(rawValue: "Neutral"), "neutral should be non-nil") + #expect(neutral == Morals.neutral, "neutral enum") + #expect(neutral.value == 0.0, "neutral value") + + let evil = try #require(Morals(rawValue: "Evil"), "evil should be non-nil") + #expect(evil == Morals.evil, "evil enum") + #expect(evil.value == -1.0, "evil value") } - func testMoralsByValue() { + @Test("Morals by value") + func moralsByValue() { let good = Morals(0.334) - XCTAssertNotNil(good, "good should be non-nil") - XCTAssertEqual(good, Morals.good, "good enum") - XCTAssertEqual(good.value, 1.0, "good value") + #expect(good == Morals.good, "good enum") + #expect(good.value == 1.0, "good value") let neutral = Morals(0.2) - XCTAssertNotNil(neutral, "neutral should be non-nil") - XCTAssertEqual(neutral, Morals.neutral, "neutral enum") - XCTAssertEqual(neutral.value, 0.0, "neutral value") + #expect(neutral == Morals.neutral, "neutral enum") + #expect(neutral.value == 0.0, "neutral value") let evil = Morals(-0.9) - XCTAssertNotNil(evil, "evil should be non-nil") - XCTAssertEqual(evil, Morals.evil, "evil enum") - XCTAssertEqual(evil.value, -1.0, "evil value") + #expect(evil == Morals.evil, "evil enum") + #expect(evil.value == -1.0, "evil value") } - func testAlignmentByType() { + @Test("Alignment by type") + func alignmentByType() { let neutralGood = Alignment.Kind(.neutral, .good) - XCTAssertEqual(neutralGood.description, "Neutral Good", "description") + #expect(neutralGood.description == "Neutral Good", "description") let neutralEvil = Alignment.Kind(.neutral, .evil) - XCTAssertEqual(neutralEvil.description, "Neutral Evil", "description") + #expect(neutralEvil.description == "Neutral Evil", "description") let chaoticNeutral = Alignment.Kind(.chaotic, .neutral) - XCTAssertEqual(chaoticNeutral.description, "Chaotic Neutral", "description") + #expect(chaoticNeutral.description == "Chaotic Neutral", "description") - XCTAssertEqual(neutralGood, Alignment.Kind(.neutral, .good), "equatable") - XCTAssertNotEqual(neutralGood, neutralEvil, "equatable") - XCTAssertNotEqual(neutralEvil, chaoticNeutral, "equatable") + #expect(neutralGood == Alignment.Kind(.neutral, .good), "equatable") + #expect(neutralGood != neutralEvil, "equatable") + #expect(neutralEvil != chaoticNeutral, "equatable") } - func testAlignmentByValue() { + @Test("Alignment by value") + func alignmentByValue() { let lawfulNeutral = Alignment(ethics: 0.7, morals: 0.0) - XCTAssertEqual(lawfulNeutral.description, "Lawful Neutral", "description") + #expect(lawfulNeutral.description == "Lawful Neutral", "description") let neutral = Alignment(ethics: 0.1, morals: -0.2) - XCTAssertEqual(neutral.description, "Neutral", "description") + #expect(neutral.description == "Neutral", "description") let chaoticGood = Alignment(ethics: -1, morals: 1) - XCTAssertEqual(chaoticGood.description, "Chaotic Good", "description") + #expect(chaoticGood.description == "Chaotic Good", "description") } - func testCHangingAlignment() { + @Test("Changing alignment") + func changingAlignment() { // Test changing alignment - do { - var alignment = Alignment(.neutral, .evil) - XCTAssertEqual(alignment.ethics, 0, "ethics value") - XCTAssertEqual(alignment.morals, -1, "morals value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.evil, "morals enumeration") + var alignment = Alignment(.neutral, .evil) + #expect(alignment.ethics == 0, "ethics value") + #expect(alignment.morals == -1, "morals value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.evil, "morals enumeration") - - alignment.morals += 0.8 - XCTAssertEqual(alignment.morals, -0.2, accuracy: 0.00001, "morals value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.neutral, "morals enumeration") + + alignment.morals += 0.8 + #expect(abs(alignment.morals - (-0.2)) < 0.00001, "morals value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.neutral, "morals enumeration") - // Try to exceed 1.0 and confirm the ethics value did not change - alignment.ethics += 5.4 - XCTAssertEqual(alignment.ethics, 0, "ethics value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.neutral, "morals enumeration") + // Try to exceed 1.0 and confirm the ethics value did not change + alignment.ethics += 5.4 + #expect(alignment.ethics == 0, "ethics value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.neutral, "morals enumeration") - alignment.morals += 0.8 - XCTAssertEqual(alignment.morals, 0.6, accuracy: 0.00001, "morals value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.good, "morals enumeration") + alignment.morals += 0.8 + #expect(abs(alignment.morals - 0.6) < 0.00001, "morals value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.good, "morals enumeration") - // Try to exceed 1.0 and confirm that morals value did not change - alignment.morals += 0.8 - XCTAssertEqual(alignment.morals, 0.6, accuracy: 0.00001, "morals value") - XCTAssertEqual(alignment.kind.ethics, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(alignment.kind.morals, Morals.good, "morals enumeration") -} + // Try to exceed 1.0 and confirm that morals value did not change + alignment.morals += 0.8 + #expect(abs(alignment.morals - 0.6) < 0.00001, "morals value") + #expect(alignment.kind.ethics == Ethics.neutral, "ethics enumeration") + #expect(alignment.kind.morals == Morals.good, "morals enumeration") } - func testAlignmentParsing() { + @Test("Alignment parsing") + func alignmentParsing() throws { // Test initializing from valid string - let neutralGood = "Neutral Good".parseAlignment - XCTAssertNotNil(neutralGood, "alignment should be non-nil") - XCTAssertEqual(neutralGood?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(neutralGood?.1, Morals.good, "morals enumeration") - - let neutral = "Neutral".parseAlignment - XCTAssertNotNil(neutral, "alignment should be non-nil") - XCTAssertEqual(neutral?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(neutral?.1, Morals.neutral, "morals enumeration") + let neutralGood = try #require("Neutral Good".parseAlignment, "alignment should be non-nil") + #expect(neutralGood.0 == Ethics.neutral, "ethics enumeration") + #expect(neutralGood.1 == Morals.good, "morals enumeration") + + let neutral = try #require("Neutral".parseAlignment, "alignment should be non-nil") + #expect(neutral.0 == Ethics.neutral, "ethics enumeration") + #expect(neutral.1 == Morals.neutral, "morals enumeration") // Test initializing from partial valid string - let chaotic = "Chaotic".parseAlignment - XCTAssertNotNil(chaotic, "alignment should be non-nil") - XCTAssertEqual(chaotic?.0, Ethics.chaotic, "ethics enumeration") - XCTAssertEqual(chaotic?.1, Morals.neutral, "morals enumeration") - - let good = "Good".parseAlignment - XCTAssertNotNil(good, "alignment should be non-nil") - XCTAssertEqual(good?.0, Ethics.neutral, "ethics enumeration") - XCTAssertEqual(good?.1, Morals.good, "morals enumeration") + let chaotic = try #require("Chaotic".parseAlignment, "alignment should be non-nil") + #expect(chaotic.0 == Ethics.chaotic, "ethics enumeration") + #expect(chaotic.1 == Morals.neutral, "morals enumeration") + + let good = try #require("Good".parseAlignment, "alignment should be non-nil") + #expect(good.0 == Ethics.neutral, "ethics enumeration") + #expect(good.1 == Morals.good, "morals enumeration") // Test initializing from bad string let tooManyWords = "Neutral Neutral Neutral".parseAlignment - XCTAssertNil(tooManyWords, "too many words should be nil") + #expect(tooManyWords == nil, "too many words should be nil") let mismatchedWords = "Foo Bar".parseAlignment - XCTAssertNil(mismatchedWords, "mismatched words should be nil") + #expect(mismatchedWords == nil, "mismatched words should be nil") let wrongWord = "Cat".parseAlignment - XCTAssertNil(wrongWord, "wrong word should be nil") + #expect(wrongWord == nil, "wrong word should be nil") } - func testAlignmentDictionaryDecoding() { + @Test("Alignment dictionary decoding") + func alignmentDictionaryDecoding() throws { let decoder = JSONDecoder() // Test initializing from dictionary of doubles @@ -188,10 +178,9 @@ class AlignmentTests: XCTestCase { } """.data(using: .utf8)! let lawfulNeutral = try? decoder.decode(Alignment.self, from: lawfulNeutralTraits) - XCTAssertNotNil(lawfulNeutral, "alignment should be non-nil") - XCTAssertEqual(lawfulNeutral?.kind.ethics, Ethics.lawful, "ethics enumeration") - XCTAssertEqual(lawfulNeutral?.kind.morals, Morals.neutral, "morals enumeration") - + let unwrappedLawfulNeutral = try #require(lawfulNeutral, "alignment should be non-nil") + #expect(unwrappedLawfulNeutral.kind.ethics == Ethics.lawful, "ethics enumeration") + #expect(unwrappedLawfulNeutral.kind.morals == Morals.neutral, "morals enumeration") } // Test initializing from dictionary of strings @@ -203,9 +192,9 @@ class AlignmentTests: XCTestCase { } """.data(using: .utf8)! let chaoticNeutral = try? decoder.decode(Alignment.self, from: chaoticNeutralTraits) - XCTAssertNotNil(chaoticNeutral, "alignment should be non-nil") - XCTAssertEqual(chaoticNeutral?.kind.ethics, Ethics.chaotic, "ethics enumeration") - XCTAssertEqual(chaoticNeutral?.kind.morals, Morals.neutral, "morals enumeration") + let unwrappedChaoticNeutral = try #require(chaoticNeutral, "alignment should be non-nil") + #expect(unwrappedChaoticNeutral.kind.ethics == Ethics.chaotic, "ethics enumeration") + #expect(unwrappedChaoticNeutral.kind.morals == Morals.neutral, "morals enumeration") } // Test initializing from bad dictionary keys @@ -217,7 +206,7 @@ class AlignmentTests: XCTestCase { } """.data(using: .utf8)! let badTrait = try? decoder.decode(Alignment.self, from: badTraitKeys) - XCTAssertNil(badTrait, "bad trait keys should be nil") + #expect(badTrait == nil, "bad trait keys should be nil") } // Test initializing from bad dictionary values @@ -229,7 +218,7 @@ class AlignmentTests: XCTestCase { } """.data(using: .utf8)! let notString = try? decoder.decode(Alignment.self, from: notStringTraits) - XCTAssertNil(notString, "non-string traits should be nil") + #expect(notString == nil, "non-string traits should be nil") let notValidTraits = """ { @@ -238,11 +227,12 @@ class AlignmentTests: XCTestCase { } """.data(using: .utf8)! let notValid = try? decoder.decode(Alignment.self, from: notValidTraits) - XCTAssertNil(notValid, "non-valid traits should be nil") + #expect(notValid == nil, "non-valid traits should be nil") } } - func testAlignmentStringDecoding() { + @Test("Alignment string decoding") + func alignmentStringDecoding() throws { let decoder = JSONDecoder() struct AlignmentContainer: Decodable { @@ -255,48 +245,41 @@ class AlignmentTests: XCTestCase { "alignment": "Chaotic Evil" } """.data(using: .utf8)! - do { - let decoded = try decoder.decode(AlignmentContainer.self, from: stringTrait) - XCTAssertEqual("\(decoded.alignment)", "Chaotic Evil", "decoded string should be round-trip") - } - catch let error { - XCTFail("Error decoding string trait: \(error)") - } + + let decoded = try decoder.decode(AlignmentContainer.self, from: stringTrait) + #expect("\(decoded.alignment)" == "Chaotic Evil", "decoded string should be round-trip") let notValidTrait = """ { "alignment": "Hello" } """.data(using: .utf8)! - do { - let notValid = try decoder.decode(AlignmentContainer.self, from: notValidTrait) - XCTAssertNil(notValid, "non-valid traits should be nil") - } - catch let error { - print("Successfully failed decoding invalid string trait: \(error)") + + // Expect this to throw an error + #expect(throws: Error.self) { + try decoder.decode(AlignmentContainer.self, from: notValidTrait) } } - func testAlignmentEncoding() { + @Test("Alignment encoding") + func alignmentEncoding() throws { let encoder = JSONEncoder() struct AlignmentContainer: Encodable { let alignment: Alignment } let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) - let encoded = try! encoder.encode(container) - let deserialized = try? JSONSerialization.jsonObject(with: encoded, options: []) - - if let dictionary = deserialized as? [String: Any] { - XCTAssertNotNil(dictionary["alignment"] as? [String: Double], "player traits round trip alignment") - if let alignment = dictionary["alignment"] as? [String: Double] { - XCTAssertEqual(alignment["ethics"], -1, "player traits round trip alignment ethics") - XCTAssertEqual(alignment["morals"], 1, "player traits round trip alignment ethics") - } - } + let encoded = try encoder.encode(container) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) + + let dictionary = try #require(deserialized as? [String: Any], "deserialized should be a dictionary") + let alignmentDict = try #require(dictionary["alignment"] as? [String: Double], "player traits round trip alignment") + #expect(alignmentDict["ethics"] == -1, "player traits round trip alignment ethics") + #expect(alignmentDict["morals"] == 1, "player traits round trip alignment morals") } - func testStringifiedEncoding() { + @Test("Stringified encoding") + func stringifiedEncoding() throws { let encoder = JSONEncoder() struct AlignmentContainer: Encodable { @@ -314,14 +297,11 @@ class AlignmentTests: XCTestCase { } let container = AlignmentContainer(alignment: Alignment(.chaotic, .good)) - let encoded = try! encoder.encode(container) - let deserialized = try? JSONSerialization.jsonObject(with: encoded, options: []) - XCTAssertNotNil(deserialized, "player traits round trip") + let encoded = try encoder.encode(container) + let deserialized = try #require(try? JSONSerialization.jsonObject(with: encoded, options: []), "player traits round trip") - if let dictionary = deserialized as? [String: String] { - if let alignment = dictionary["alignment"] { - XCTAssertEqual(alignment, "Chaotic Good", "player traits round trip alignment ethics") - } - } + let dictionary = try #require(deserialized as? [String: String], "should be string dictionary") + let alignment = try #require(dictionary["alignment"], "alignment should exist") + #expect(alignment == "Chaotic Good", "player traits round trip alignment") } } diff --git a/RolePlayingCore/RolePlayingCoreTests/BackgroundsTests.swift b/RolePlayingCore/RolePlayingCoreTests/BackgroundsTests.swift index f39ce62..e265fc9 100644 --- a/RolePlayingCore/RolePlayingCoreTests/BackgroundsTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/BackgroundsTests.swift @@ -6,12 +6,16 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // -import XCTest +import Testing import RolePlayingCore -class BackgroundsTests: XCTestCase { +@Suite("Backgrounds Tests") +struct BackgroundsTests { - func testBackgroundTraitsDecoding() throws { + let decoder = JSONDecoder() + + @Test("Decode background traits") + func backgroundTraitsDecoding() async throws { // Given: JSON data representing a background let jsonData = """ { @@ -24,23 +28,22 @@ class BackgroundsTests: XCTestCase { } """.data(using: .utf8)! - let decoder = JSONDecoder() - // When: Decoding the JSON data let background = try decoder.decode(BackgroundTraits.self, from: jsonData) // Then: The properties should match the input - XCTAssertEqual(background.name, "Acolyte", "Name should match") - XCTAssertEqual(background.abilityScores, ["Intelligence", "Wisdom"], "Ability scores should match") - XCTAssertEqual(background.feat, "Magic Initiate", "Feat should match") - XCTAssertEqual(background.skillProficiencies.count, 2, "Should have 2 skill proficiencies") - XCTAssertEqual(background.skillProficiencies.skillNames, ["Insight", "Religion"], "Skill names should match") - XCTAssertEqual(background.toolProficiency, "Calligrapher's Supplies", "Tool proficiency should match") - XCTAssertEqual(background.equipment.count, 2, "Should have 2 equipment choices") - XCTAssertEqual(background.equipment[0], ["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], "First equipment choice should match") + #expect(background.name == "Acolyte", "Name should match") + #expect(background.abilityScores == ["Intelligence", "Wisdom"], "Ability scores should match") + #expect(background.feat == "Magic Initiate", "Feat should match") + #expect(background.skillProficiencies.count == 2, "Should have 2 skill proficiencies") + #expect(background.skillProficiencies.skillNames == ["Insight", "Religion"], "Skill names should match") + #expect(background.toolProficiency == "Calligrapher's Supplies", "Tool proficiency should match") + #expect(background.equipment.count == 2, "Should have 2 equipment choices") + #expect(background.equipment[0] == ["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], "First equipment choice should match") } - func testBackgroundTraitsEncoding() throws { + @Test("Encode background traits with round-trip") + func backgroundTraitsEncoding() async throws { // Given: A BackgroundTraits instance let jsonData = """ { @@ -53,7 +56,6 @@ class BackgroundsTests: XCTestCase { } """.data(using: .utf8)! - let decoder = JSONDecoder() let background = try decoder.decode(BackgroundTraits.self, from: jsonData) // When: Encoding the background back to JSON @@ -64,15 +66,16 @@ class BackgroundsTests: XCTestCase { // Then: The encoded data should be decodable and match the original let decodedBackground = try decoder.decode(BackgroundTraits.self, from: encodedData) - XCTAssertEqual(decodedBackground.name, background.name, "Name should match after round-trip") - XCTAssertEqual(decodedBackground.abilityScores, background.abilityScores, "Ability scores should match after round-trip") - XCTAssertEqual(decodedBackground.feat, background.feat, "Feat should match after round-trip") - XCTAssertEqual(decodedBackground.skillProficiencies.skillNames, background.skillProficiencies.skillNames, "Skills should match after round-trip") - XCTAssertEqual(decodedBackground.toolProficiency, background.toolProficiency, "Tool proficiency should match after round-trip") - XCTAssertEqual(decodedBackground.equipment, background.equipment, "Equipment should match after round-trip") + #expect(decodedBackground.name == background.name, "Name should match after round-trip") + #expect(decodedBackground.abilityScores == background.abilityScores, "Ability scores should match after round-trip") + #expect(decodedBackground.feat == background.feat, "Feat should match after round-trip") + #expect(decodedBackground.skillProficiencies.skillNames == background.skillProficiencies.skillNames, "Skills should match after round-trip") + #expect(decodedBackground.toolProficiency == background.toolProficiency, "Tool proficiency should match after round-trip") + #expect(decodedBackground.equipment == background.equipment, "Equipment should match after round-trip") } - func testBackgroundsCollection() throws { + @Test("Decode and query backgrounds collection") + func backgroundsCollection() async throws { // Given: JSON data representing a collection of backgrounds let jsonData = """ { @@ -105,36 +108,30 @@ class BackgroundsTests: XCTestCase { } """.data(using: .utf8)! - let decoder = JSONDecoder() - // When: Decoding the JSON data into a Backgrounds collection let backgrounds = try decoder.decode(Backgrounds.self, from: jsonData) // Then: The collection should have the correct count - XCTAssertEqual(backgrounds.count, 3, "Should have 3 backgrounds") + #expect(backgrounds.count == 3, "Should have 3 backgrounds") // Then: The find method should locate backgrounds by name - let acolyte = backgrounds.find("Acolyte") - XCTAssertNotNil(acolyte, "Should find Acolyte background") - XCTAssertEqual(acolyte?.name, "Acolyte", "Found background should be Acolyte") - XCTAssertEqual(acolyte?.feat, "Magic Initiate", "Acolyte feat should match") + let acolyte = try #require(backgrounds.find("Acolyte")) + #expect(acolyte.name == "Acolyte", "Found background should be Acolyte") + #expect(acolyte.feat == "Magic Initiate", "Acolyte feat should match") - let criminal = backgrounds.find("Criminal") - XCTAssertNotNil(criminal, "Should find Criminal background") - XCTAssertEqual(criminal?.skillProficiencies.skillNames, ["Deception", "Stealth"], "Criminal skills should match") + let criminal = try #require(backgrounds.find("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") - XCTAssertNil(nonExistent, "Should not find non-existent background") + #expect(nonExistent == nil, "Should not find non-existent background") // Then: Subscript access should work correctly - let firstBackground = backgrounds[0] - XCTAssertNotNil(firstBackground, "Should access first background") - XCTAssertEqual(firstBackground?.name, "Acolyte", "First background should be Acolyte") + let firstBackground = try #require(backgrounds[0]) + #expect(firstBackground.name == "Acolyte", "First background should be Acolyte") - let thirdBackground = backgrounds[2] - XCTAssertNotNil(thirdBackground, "Should access third background") - XCTAssertEqual(thirdBackground?.name, "Soldier", "Third background should be Soldier") + let thirdBackground = try #require(backgrounds[2]) + #expect(thirdBackground.name == "Soldier", "Third background should be Soldier") // Then: Round-trip encoding should preserve data let encoder = JSONEncoder() @@ -142,8 +139,8 @@ class BackgroundsTests: XCTestCase { let encodedData = try encoder.encode(backgrounds) let decodedBackgrounds = try decoder.decode(Backgrounds.self, from: encodedData) - XCTAssertEqual(decodedBackgrounds.count, backgrounds.count, "Count should match after round-trip") - XCTAssertEqual(decodedBackgrounds.find("Criminal")?.name, "Criminal", "Should find Criminal after round-trip") - XCTAssertEqual(decodedBackgrounds[1]?.toolProficiency, backgrounds[1]?.toolProficiency, "Tool proficiency should match after round-trip") + #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") } } diff --git a/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift b/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift index d09fd1c..6382cca 100644 --- a/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift @@ -12,7 +12,7 @@ import RolePlayingCore class CharacterGeneratorTests: XCTestCase { - let bundle = Bundle(for: CharacterGeneratorTests.self) + let bundle = testBundle let sampleSize = 256 diff --git a/RolePlayingCore/RolePlayingCoreTests/ClassTraitsTests.swift b/RolePlayingCore/RolePlayingCoreTests/ClassTraitsTests.swift index c77f985..b03347a 100644 --- a/RolePlayingCore/RolePlayingCoreTests/ClassTraitsTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/ClassTraitsTests.swift @@ -6,18 +6,85 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest +import Testing import RolePlayingCore -class ClassTraitsTests: XCTestCase { +@Suite("ClassTraits Tests") +struct ClassTraitsTests { let decoder = JSONDecoder() - func testDecodingClassTraits() { - // Test nominal required traits - do { - let traits = """ + @Test("Decoding class traits with nominal required traits") + func decodingNominalClassTraits() throws { + let traits = """ + { + "name": "Fighter", + "plural": "Fighters", + "hit dice": "d10", + "primary ability": ["Strength"], + "alternate primary ability": ["Dexterity"], + "saving throws": ["Strength", "Constitution"], + "starting wealth": "5d4x10" + } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + + #expect(classTraits.name == "Fighter", "name") + #expect(classTraits.plural == "Fighters", "plural") + let hitDice = classTraits.hitDice as? SimpleDice + #expect(hitDice != nil, "hit dice") + #expect(hitDice?.sides == 10, "hit dice sides") + #expect(hitDice?.times == 1, "hit dice times") + + let primaryAbility: [Ability] = classTraits.primaryAbility + #expect(primaryAbility == [Ability("Strength")], "primary ability") + + let savingThrows: [Ability] = classTraits.savingThrows + #expect(savingThrows == [Ability("Strength"), Ability("Constitution")], "saving throws") + + let startingWealth = classTraits.startingWealth as? CompoundDice + #expect(startingWealth != nil, "starting wealth") + + #expect(classTraits.experiencePoints == nil, "experience points") + } + + @Test("Decoding class traits with minimum required traits") + func decodingMinimumClassTraits() throws { + let traits = """ + { + "name": "Fighter", + "plural": "Fighters", + "hit dice": "d10", + "starting wealth": "5d4x10" + } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + + #expect(classTraits.name == "Fighter", "name") + #expect(classTraits.plural == "Fighters", "plural") + let hitDice = classTraits.hitDice as? SimpleDice + #expect(hitDice != nil, "hit dice") + #expect(hitDice?.sides == 10, "hit dice sides") + #expect(hitDice?.times == 1, "hit dice times") + + let primaryAbility: [Ability] = classTraits.primaryAbility + #expect(primaryAbility.count == 0, "primary ability") + + let savingThrows: [Ability] = classTraits.savingThrows + #expect(savingThrows.count == 0, "saving throws") + + let startingWealth = classTraits.startingWealth as? CompoundDice + #expect(startingWealth != nil, "starting wealth") + + #expect(classTraits.experiencePoints == nil, "experience points") + } + + @Test("Decoding class traits with optional experience points") + func decodingClassTraitsWithExperiencePoints() throws { + let traits = """ { "name": "Fighter", "plural": "Fighters", @@ -25,131 +92,41 @@ class ClassTraitsTests: XCTestCase { "primary ability": ["Strength"], "alternate primary ability": ["Dexterity"], "saving throws": ["Strength", "Constitution"], - "starting wealth": "5d4x10" + "starting wealth": "5d4x10", + "experience points": [300, 900, 2700] } """.data(using: .utf8)! - do { - let classTraits = try decoder.decode(ClassTraits.self, from: traits) - - XCTAssertEqual(classTraits.name, "Fighter", "name") - XCTAssertEqual(classTraits.plural, "Fighters", "plural") - let hitDice = classTraits.hitDice as? SimpleDice - XCTAssertNotNil(hitDice, "hit dice") - XCTAssertEqual(hitDice?.sides, 10, "hit dice sides") - XCTAssertEqual(hitDice?.times, 1, "hit dice times") - - let primaryAbility: [Ability] = classTraits.primaryAbility - XCTAssertEqual(primaryAbility, [Ability("Strength")], "primary ability") - - let savingThrows: [Ability] = classTraits.savingThrows - XCTAssertEqual(savingThrows, [Ability("Strength"), Ability("Constitution")], "saving throws") - - let startingWealth = classTraits.startingWealth as? CompoundDice - XCTAssertNotNil(startingWealth, "starting wealth") - - XCTAssertNil(classTraits.experiencePoints, "experience points") - } - catch let error { - XCTFail("Error decoding classTraits: \(error)") - } - } - // Test minimum required traits - do { - let traits = """ - { - "name": "Fighter", - "plural": "Fighters", - "hit dice": "d10", - "starting wealth": "5d4x10" - } - """.data(using: .utf8)! - do { - let classTraits = try decoder.decode(ClassTraits.self, from: traits) - - XCTAssertNotNil(classTraits) - XCTAssertEqual(classTraits.name, "Fighter", "name") - XCTAssertEqual(classTraits.plural, "Fighters", "plural") - let hitDice = classTraits.hitDice as? SimpleDice - XCTAssertNotNil(hitDice, "hit dice") - XCTAssertEqual(hitDice?.sides, 10, "hit dice sides") - XCTAssertEqual(hitDice?.times, 1, "hit dice times") - - let primaryAbility: [Ability] = classTraits.primaryAbility - XCTAssertEqual(primaryAbility.count, 0, "primary ability") - - let savingThrows: [Ability] = classTraits.savingThrows - XCTAssertEqual(savingThrows.count, 0, "saving throws") - - let startingWealth = classTraits.startingWealth as? CompoundDice - XCTAssertNotNil(startingWealth, "starting wealth") - - XCTAssertNil(classTraits.experiencePoints, "experience points") - } - catch let error { - XCTFail("Error decoding classTraits: \(error)") - } - } + let classTraits = try decoder.decode(ClassTraits.self, from: traits) - // Test traits with optional experience points - do { - var classTraits: ClassTraits! = nil - - let traits = """ - { - "name": "Fighter", - "plural": "Fighters", - "hit dice": "d10", - "primary ability": ["Strength"], - "alternate primary ability": ["Dexterity"], - "saving throws": ["Strength", "Constitution"], - "starting wealth": "5d4x10", - "experience points": [300, 900, 2700] - } - """.data(using: .utf8)! - do { - classTraits = try decoder.decode(ClassTraits.self, from: traits) - - let experiencePoints: [Int] = classTraits?.experiencePoints ?? [] - XCTAssertEqual(experiencePoints, [300, 900, 2700], "experience points") - } - catch let error { - XCTFail("Error decoding classTraits: \(error)") - } - } + let experiencePoints: [Int] = classTraits.experiencePoints ?? [] + #expect(experiencePoints == [300, 900, 2700], "experience points") } - func testEncodingClassTraits() { + @Test("Encoding class traits with required traits") + func encodingClassTraits() throws { let encoder = JSONEncoder() - // Test encoding required traits - do { - let classTraits = ClassTraits(name: "Fighter", - plural: "Fighters", - hitDice: SimpleDice(.d10), - startingWealth: CompoundDice(.d4, times: 5, modifier: 10, mathOperator: "x")) - - do { - let encoded = try encoder.encode(classTraits) - let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: Any] - XCTAssertEqual(dictionary?["name"] as? String, "Fighter", "name") - XCTAssertEqual(dictionary?["plural"] as? String, "Fighters", "plural") - XCTAssertEqual(dictionary?["hit dice"] as? String, "d10", "hit dice") - XCTAssertEqual(dictionary?["starting wealth"] as? String, "5d4x10", "starting wealth") - } - catch let error { - XCTFail("Error decoding classTraits: \(error)") - } - } + let classTraits = ClassTraits(name: "Fighter", + plural: "Fighters", + hitDice: SimpleDice(.d10), + startingWealth: CompoundDice(.d4, times: 5, modifier: 10, mathOperator: "x")) + + let encoded = try encoder.encode(classTraits) + let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: Any] + #expect(dictionary?["name"] as? String == "Fighter", "name") + #expect(dictionary?["plural"] as? String == "Fighters", "plural") + #expect(dictionary?["hit dice"] as? String == "d10", "hit dice") + #expect(dictionary?["starting wealth"] as? String == "5d4x10", "starting wealth") } - func testMissingClassTraits() { - + @Test("Missing class traits should fail to decode") + func missingClassTraitsFailDecoding() { // Test that each missing trait results in nil do { let traits = "{}".data(using: .utf8)! let classTraits = try? decoder.decode(ClassTraits.self, from: traits) - XCTAssertNil(classTraits) + #expect(classTraits == nil) } do { @@ -159,10 +136,9 @@ class ClassTraitsTests: XCTestCase { } """.data(using: .utf8)! let classTraits = try? decoder.decode(ClassTraits.self, from: traits) - XCTAssertNil(classTraits) + #expect(classTraits == nil) } - do { let traits = """ { @@ -171,7 +147,7 @@ class ClassTraitsTests: XCTestCase { } """.data(using: .utf8)! let classTraits = try? decoder.decode(ClassTraits.self, from: traits) - XCTAssertNil(classTraits) + #expect(classTraits == nil) } do { @@ -183,184 +159,165 @@ class ClassTraitsTests: XCTestCase { } """.data(using: .utf8)! let classTraits = try? decoder.decode(ClassTraits.self, from: traits) - XCTAssertNil(classTraits) + #expect(classTraits == nil) } } // MARK: - Edge Case Tests - func testExperiencePointsEdgeCases() { - // Test with empty experience points array - do { - let traits = """ - { - "name": "Novice", - "plural": "Novices", - "hit dice": "d6", - "starting wealth": "2d4x10", - "experience points": [] - } - """.data(using: .utf8)! - - let classTraits = try decoder.decode(ClassTraits.self, from: traits) - XCTAssertNotNil(classTraits.experiencePoints, "Should decode empty array") - XCTAssertEqual(classTraits.experiencePoints?.count, 0, "Should have 0 experience points") - XCTAssertEqual(classTraits.maxLevel, 0, "Max level should be 0 for empty array") - XCTAssertEqual(classTraits.minExperiencePoints(at: 1), 0, "Min XP should be 0") - XCTAssertEqual(classTraits.maxExperiencePoints(at: 1), -1, "Max XP should be -1") - } catch { - XCTFail("Failed to decode: \(error)") + @Test("Experience points edge cases - empty array") + func experiencePointsEmptyArray() throws { + let traits = """ + { + "name": "Novice", + "plural": "Novices", + "hit dice": "d6", + "starting wealth": "2d4x10", + "experience points": [] } + """.data(using: .utf8)! - // Test with single level experience points - do { - let traits = """ - { - "name": "Novice", - "plural": "Novices", - "hit dice": "d6", - "starting wealth": "2d4x10", - "experience points": [0] - } - """.data(using: .utf8)! - - let classTraits = try decoder.decode(ClassTraits.self, from: traits) - XCTAssertEqual(classTraits.maxLevel, 1, "Max level should be 1") - XCTAssertEqual(classTraits.minExperiencePoints(at: 1), 0, "Min XP at level 1 should be 0") - XCTAssertEqual(classTraits.minExperiencePoints(at: 2), 0, "Beyond max level should return last value") - } catch { - XCTFail("Failed to decode: \(error)") + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + #expect(classTraits.experiencePoints != nil, "Should decode empty array") + #expect(classTraits.experiencePoints?.count == 0, "Should have 0 experience points") + #expect(classTraits.maxLevel == 0, "Max level should be 0 for empty array") + #expect(classTraits.minExperiencePoints(at: 1) == 0, "Min XP should be 0") + #expect(classTraits.maxExperiencePoints(at: 1) == -1, "Max XP should be -1") + } + + @Test("Experience points edge cases - single level") + func experiencePointsSingleLevel() throws { + let traits = """ + { + "name": "Novice", + "plural": "Novices", + "hit dice": "d6", + "starting wealth": "2d4x10", + "experience points": [0] } + """.data(using: .utf8)! - // Test level 0 and negative levels - do { - let classTraits = ClassTraits( - name: "Test", - plural: "Tests", - hitDice: SimpleDice(.d8), - startingWealth: SimpleDice(.d4), - experiencePoints: [0, 300, 900, 2700] - ) - - XCTAssertEqual(classTraits.minExperiencePoints(at: 0), 0, "Level 0 should map to level 1") - XCTAssertEqual(classTraits.minExperiencePoints(at: -5), 0, "Negative level should map to level 1") - XCTAssertEqual(classTraits.maxExperiencePoints(at: 0), 0, "Max XP at level 0 should work") - } + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + #expect(classTraits.maxLevel == 1, "Max level should be 1") + #expect(classTraits.minExperiencePoints(at: 1) == 0, "Min XP at level 1 should be 0") + #expect(classTraits.minExperiencePoints(at: 2) == 0, "Beyond max level should return last value") + } + + @Test("Experience points edge cases - level 0 and negative levels") + func experiencePointsInvalidLevels() { + let classTraits = ClassTraits( + name: "Test", + plural: "Tests", + hitDice: SimpleDice(.d8), + startingWealth: SimpleDice(.d4), + experiencePoints: [0, 300, 900, 2700] + ) - // Test beyond max level - do { - let classTraits = ClassTraits( - name: "Test", - plural: "Tests", - hitDice: SimpleDice(.d8), - startingWealth: SimpleDice(.d4), - experiencePoints: [0, 300, 900] - ) - - XCTAssertEqual(classTraits.maxLevel, 3, "Max level should be 3") - XCTAssertEqual(classTraits.minExperiencePoints(at: 10), 900, "Beyond max should return last value") - XCTAssertEqual(classTraits.maxExperiencePoints(at: 3), 899, "Max XP at level 3") - } + #expect(classTraits.minExperiencePoints(at: 0) == 0, "Level 0 should map to level 1") + #expect(classTraits.minExperiencePoints(at: -5) == 0, "Negative level should map to level 1") + #expect(classTraits.maxExperiencePoints(at: 0) == 0, "Max XP at level 0 should work") } - func testEmptyAndNilOptionalFields() { - // Test with all optional fields as empty arrays/dictionaries - do { - let traits = """ - { - "name": "Minimalist", - "plural": "Minimalists", - "hit dice": "d8", - "starting wealth": "3d4x10", - "descriptive traits": {}, - "primary ability": [], - "saving throws": [], - "starting skill count": 0, - "skill proficiencies": [], - "weapon proficiencies": [], - "tool proficiencies": [], - "armor training": [], - "starting equipment": [] - } - """.data(using: .utf8)! - - let classTraits = try decoder.decode(ClassTraits.self, from: traits) - - XCTAssertEqual(classTraits.descriptiveTraits.count, 0, "Descriptive traits should be empty") - XCTAssertEqual(classTraits.primaryAbility.count, 0, "Primary ability should be empty") - XCTAssertNil(classTraits.alternatePrimaryAbility, "Alternate primary ability should be nil") - XCTAssertEqual(classTraits.savingThrows.count, 0, "Saving throws should be empty") - XCTAssertEqual(classTraits.startingSkillCount, 0, "Starting skill count should be 0") - XCTAssertEqual(classTraits.skillProficiencies.count, 0, "Skill proficiencies should be empty") - XCTAssertEqual(classTraits.weaponProficiencies.count, 0, "Weapon proficiencies should be empty") - XCTAssertEqual(classTraits.toolProficiencies.count, 0, "Tool proficiencies should be empty") - XCTAssertEqual(classTraits.armorTraining.count, 0, "Armor training should be empty") - XCTAssertEqual(classTraits.startingEquipment.count, 0, "Starting equipment should be empty") - XCTAssertNil(classTraits.experiencePoints, "Experience points should be nil") - } catch { - XCTFail("Failed to decode: \(error)") + @Test("Experience points edge cases - beyond max level") + func experiencePointsBeyondMaxLevel() { + let classTraits = ClassTraits( + name: "Test", + plural: "Tests", + hitDice: SimpleDice(.d8), + startingWealth: SimpleDice(.d4), + experiencePoints: [0, 300, 900] + ) + + #expect(classTraits.maxLevel == 3, "Max level should be 3") + #expect(classTraits.minExperiencePoints(at: 10) == 900, "Beyond max should return last value") + #expect(classTraits.maxExperiencePoints(at: 3) == 899, "Max XP at level 3") + } + + @Test("Empty and nil optional fields") + func emptyOptionalFields() throws { + let traits = """ + { + "name": "Minimalist", + "plural": "Minimalists", + "hit dice": "d8", + "starting wealth": "3d4x10", + "descriptive traits": {}, + "primary ability": [], + "saving throws": [], + "starting skill count": 0, + "skill proficiencies": [], + "weapon proficiencies": [], + "tool proficiencies": [], + "armor training": [], + "starting equipment": [] } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + + #expect(classTraits.descriptiveTraits.count == 0, "Descriptive traits should be empty") + #expect(classTraits.primaryAbility.count == 0, "Primary ability should be empty") + #expect(classTraits.alternatePrimaryAbility == nil, "Alternate primary ability should be nil") + #expect(classTraits.savingThrows.count == 0, "Saving throws should be empty") + #expect(classTraits.startingSkillCount == 0, "Starting skill count should be 0") + #expect(classTraits.skillProficiencies.count == 0, "Skill proficiencies should be empty") + #expect(classTraits.weaponProficiencies.count == 0, "Weapon proficiencies should be empty") + #expect(classTraits.toolProficiencies.count == 0, "Tool proficiencies should be empty") + #expect(classTraits.armorTraining.count == 0, "Armor training should be empty") + #expect(classTraits.startingEquipment.count == 0, "Starting equipment should be empty") + #expect(classTraits.experiencePoints == nil, "Experience points should be nil") } - func testMultiplePrimaryAbilities() { - // Test with multiple primary and alternate abilities - do { - let traits = """ - { - "name": "Ranger", - "plural": "Rangers", - "hit dice": "d10", - "starting wealth": "5d4x10", - "primary ability": ["Strength", "Dexterity"], - "alternate primary ability": ["Constitution", "Wisdom"] - } - """.data(using: .utf8)! - - let classTraits = try decoder.decode(ClassTraits.self, from: traits) - - XCTAssertEqual(classTraits.primaryAbility.count, 2, "Should have 2 primary abilities") - XCTAssertEqual(classTraits.primaryAbility, [Ability("Strength"), Ability("Dexterity")]) - XCTAssertEqual(classTraits.alternatePrimaryAbility?.count, 2, "Should have 2 alternate abilities") - XCTAssertEqual(classTraits.alternatePrimaryAbility, [Ability("Constitution"), Ability("Wisdom")]) - } catch { - XCTFail("Failed to decode: \(error)") + @Test("Multiple primary abilities") + func multiplePrimaryAbilities() throws { + let traits = """ + { + "name": "Ranger", + "plural": "Rangers", + "hit dice": "d10", + "starting wealth": "5d4x10", + "primary ability": ["Strength", "Dexterity"], + "alternate primary ability": ["Constitution", "Wisdom"] } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + + #expect(classTraits.primaryAbility.count == 2, "Should have 2 primary abilities") + #expect(classTraits.primaryAbility == [Ability("Strength"), Ability("Dexterity")]) + #expect(classTraits.alternatePrimaryAbility?.count == 2, "Should have 2 alternate abilities") + #expect(classTraits.alternatePrimaryAbility == [Ability("Constitution"), Ability("Wisdom")]) } - func testNestedStartingEquipment() { - // Test with complex nested equipment choices - do { - let traits = """ - { - "name": "Paladin", - "plural": "Paladins", - "hit dice": "d10", - "starting wealth": "5d4x10", - "starting equipment": [ - ["Longsword", "Shield"], - ["Greatsword"], - ["5 Javelins", "Simple Weapon"], - ["Priest's Pack", "Explorer's Pack"], - ["Chain Mail", "Holy Symbol"] - ] - } - """.data(using: .utf8)! - - let classTraits = try decoder.decode(ClassTraits.self, from: traits) - - XCTAssertEqual(classTraits.startingEquipment.count, 5, "Should have 5 equipment choices") - XCTAssertEqual(classTraits.startingEquipment[0], ["Longsword", "Shield"]) - XCTAssertEqual(classTraits.startingEquipment[1], ["Greatsword"]) - XCTAssertEqual(classTraits.startingEquipment[3], ["Priest's Pack", "Explorer's Pack"]) - } catch { - XCTFail("Failed to decode: \(error)") + @Test("Nested starting equipment") + func nestedStartingEquipment() throws { + let traits = """ + { + "name": "Paladin", + "plural": "Paladins", + "hit dice": "d10", + "starting wealth": "5d4x10", + "starting equipment": [ + ["Longsword", "Shield"], + ["Greatsword"], + ["5 Javelins", "Simple Weapon"], + ["Priest's Pack", "Explorer's Pack"], + ["Chain Mail", "Holy Symbol"] + ] } + """.data(using: .utf8)! + + let classTraits = try decoder.decode(ClassTraits.self, from: traits) + + #expect(classTraits.startingEquipment.count == 5, "Should have 5 equipment choices") + #expect(classTraits.startingEquipment[0] == ["Longsword", "Shield"]) + #expect(classTraits.startingEquipment[1] == ["Greatsword"]) + #expect(classTraits.startingEquipment[3] == ["Priest's Pack", "Explorer's Pack"]) } - func testRoundTripEncodingWithAllFields() { - // Test complete round-trip encoding/decoding with all fields populated + @Test("Round-trip encoding with all fields") + func roundTripEncodingWithAllFields() throws { let encoder = JSONEncoder() - encoder.outputFormatting = .sortedKeys let original = ClassTraits( name: "Bard", @@ -380,26 +337,22 @@ class ClassTraitsTests: XCTestCase { experiencePoints: [0, 300, 900, 2700, 6500, 14000] ) - do { - let encoded = try encoder.encode(original) - let decoded = try decoder.decode(ClassTraits.self, from: encoded) - - XCTAssertEqual(decoded.name, original.name, "Name should match after round-trip") - XCTAssertEqual(decoded.plural, original.plural, "Plural should match") - XCTAssertEqual(decoded.primaryAbility, original.primaryAbility, "Primary ability should match") - XCTAssertEqual(decoded.alternatePrimaryAbility, original.alternatePrimaryAbility, "Alternate ability should match") - XCTAssertEqual(decoded.savingThrows, original.savingThrows, "Saving throws should match") - XCTAssertEqual(decoded.startingSkillCount, original.startingSkillCount, "Skill count should match") - XCTAssertEqual(decoded.skillProficiencies, original.skillProficiencies, "Skill proficiencies should match") - XCTAssertEqual(decoded.weaponProficiencies, original.weaponProficiencies, "Weapon proficiencies should match") - XCTAssertEqual(decoded.toolProficiencies, original.toolProficiencies, "Tool proficiencies should match") - XCTAssertEqual(decoded.armorTraining, original.armorTraining, "Armor training should match") - XCTAssertEqual(decoded.startingEquipment, original.startingEquipment, "Starting equipment should match") - XCTAssertEqual(decoded.experiencePoints, original.experiencePoints, "Experience points should match") - XCTAssertEqual(decoded.descriptiveTraits.count, original.descriptiveTraits.count, "Descriptive traits count should match") - XCTAssertEqual(decoded.maxLevel, 6, "Max level should be 6") - } catch { - XCTFail("Failed round-trip encoding/decoding: \(error)") - } + let encoded = try encoder.encode(original) + let decoded = try decoder.decode(ClassTraits.self, from: encoded) + + #expect(decoded.name == original.name, "Name should match after round-trip") + #expect(decoded.plural == original.plural, "Plural should match") + #expect(decoded.primaryAbility == original.primaryAbility, "Primary ability should match") + #expect(decoded.alternatePrimaryAbility == original.alternatePrimaryAbility, "Alternate ability should match") + #expect(decoded.savingThrows == original.savingThrows, "Saving throws should match") + #expect(decoded.startingSkillCount == original.startingSkillCount, "Skill count should match") + #expect(decoded.skillProficiencies == original.skillProficiencies, "Skill proficiencies should match") + #expect(decoded.weaponProficiencies == original.weaponProficiencies, "Weapon proficiencies should match") + #expect(decoded.toolProficiencies == original.toolProficiencies, "Tool proficiencies should match") + #expect(decoded.armorTraining == original.armorTraining, "Armor training should match") + #expect(decoded.startingEquipment == original.startingEquipment, "Starting equipment should match") + #expect(decoded.experiencePoints == original.experiencePoints, "Experience points should match") + #expect(decoded.descriptiveTraits.count == original.descriptiveTraits.count, "Descriptive traits count should match") + #expect(decoded.maxLevel == 6, "Max level should be 6") } } diff --git a/RolePlayingCore/RolePlayingCoreTests/ClassesTests.swift b/RolePlayingCore/RolePlayingCoreTests/ClassesTests.swift index 19b97f7..d6a22f7 100644 --- a/RolePlayingCore/RolePlayingCoreTests/ClassesTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/ClassesTests.swift @@ -6,47 +6,38 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest +import Testing import RolePlayingCore -class ClassesTests: XCTestCase { +@Suite("Classes Tests") +struct ClassesTests { - let bundle = Bundle(for: ClassesTests.self) + let bundle = testBundle let decoder = JSONDecoder() - func testDefaultClasses() { - do { - let jsonData = try bundle.loadJSON("TestClasses") - let classes = try decoder.decode(Classes.self, from: jsonData) - XCTAssertEqual(classes.classes.count, 4, "classes count failed") - XCTAssertEqual(classes.count, 4, "classes count failed") - XCTAssertNotNil(classes[0], "class by index failed") - - XCTAssertEqual(classes.experiencePoints?.count, 20, "array of experience points failed") - - // Test finding a class by name - XCTAssertNotNil(classes.find("Fighter"), "Fighter should be non-nil") - XCTAssertNil(classes.find("Foo"), "Foo should be nil") - XCTAssertNil(classes.find(nil), "nil class name should find nil") - } - catch { - XCTFail("Classes threw an error: \(error)") - } + @Test("Default classes") + func defaultClasses() throws { + let jsonData = try bundle.loadJSON("TestClasses") + let classes = try decoder.decode(Classes.self, from: jsonData) + #expect(classes.classes.count == 4, "classes count failed") + #expect(classes.count == 4, "classes count failed") + #expect(classes[0] != nil, "class by index failed") + #expect(classes.experiencePoints?.count == 20, "array of experience points failed") + + // Test finding a class by name + #expect(classes.find("Fighter") != nil, "Fighter should be non-nil") + #expect(classes.find("Foo") == nil, "Foo should be nil") + #expect(classes.find(nil) == nil, "nil class name should find nil") } - func testUncommonClasses() { - var classes: Classes! = nil - do { - let jsonData = try bundle.loadJSON("TestMoreClasses") - classes = try decoder.decode(Classes.self, from: jsonData) - } - catch let error { - XCTFail("Classes threw an error: \(error)") - } + @Test("Uncommon classes") + func uncommonClasses() throws { + let jsonData = try bundle.loadJSON("TestMoreClasses") + let classes = try decoder.decode(Classes.self, from: jsonData) - XCTAssertEqual(classes.classes.count, 8, "classes count failed") + #expect(classes.classes.count == 8, "classes count failed") } } diff --git a/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift b/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift index 25a2f1f..7dafb8c 100644 --- a/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/ConfigurationTests.swift @@ -6,49 +6,41 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class ConfigurationTests: XCTestCase { +@Suite("Configuration Tests") +struct ConfigurationTests { + + let bundle = testBundle - func testDefaultConfiguration() { - do { - let bundle = Bundle(for: ConfigurationTests.self) - let configuration = try Configuration("TestConfiguration", from: bundle) + @Test("Default configuration loads successfully") + func defaultConfiguration() async throws { + let configuration = try Configuration("TestConfiguration", from: bundle) + + // Print an ability score summary for edification + print("Ability Score Summary:") + let abilities = Ability.defaults + for ability in abilities { + var importantFor = [String]() - // Print an ability score summary for edification - print("Ability Score Summary:") - let abilities = Ability.defaults - for ability in abilities { - var importantFor = [String]() - - for classTraits in configuration.classes.classes { - if classTraits.primaryAbility.contains(ability) { - importantFor.append(classTraits.name) - } + for classTraits in configuration.classes.classes { + if classTraits.primaryAbility.contains(ability) { + importantFor.append(classTraits.name) } - - // TODO: make into assertions. For now, visually compare results. - print(ability.name) - let important = importantFor.count == 0 ? ["Everyone"] : importantFor - print("Important for: \(important)") } - } - catch let error { - XCTFail("Configuration threw an error: \(error)") + + // TODO: make into assertions. For now, visually compare results. + print(ability.name) + let important = importantFor.count == 0 ? ["Everyone"] : importantFor + print("Important for: \(important)") } } - func testInvalidConfiguration() { - do { - let bundle = Bundle(for: ConfigurationTests.self) - + @Test("Invalid configuration throws error") + func invalidConfiguration() async throws { + #expect(throws: (any Error).self) { _ = try Configuration("InvalidConfiguration", from: bundle) - XCTFail("Invalid configuration should have thrown an error") - } - catch let error { - print("Invalid configuration correctly threw an error: \(error)") } } } diff --git a/RolePlayingCore/RolePlayingCoreTests/CurrencyTests.swift b/RolePlayingCore/RolePlayingCoreTests/CurrencyTests.swift index b4a6c4d..db6d70e 100644 --- a/RolePlayingCore/RolePlayingCoreTests/CurrencyTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/CurrencyTests.swift @@ -6,27 +6,25 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing @testable import RolePlayingCore -class UnitCurrencyTests: XCTestCase { +@Suite("Currency Tests") +struct UnitCurrencyTests { - static var currencies: Currencies! + let bundle = testBundle + let decoder = JSONDecoder() + let currencies: Currencies - override class func setUp() { - super.setUp() - + init() throws { // Only load once. TODO: this has a side effect on other unit tests: currencies are already loaded. - let bundle = Bundle(for: UnitCurrencyTests.self) - let decoder = JSONDecoder() let data = try! bundle.loadJSON("TestCurrencies") - - currencies = try! decoder.decode(Currencies.self, from: data) + self.currencies = try! decoder.decode(Currencies.self, from: data) } - func testUnitCurrency() { - XCTAssertEqual(UnitCurrency.baseUnit(), Currencies.find("gp"), "base unit should be goldPieces") + @Test("Unit currency calculations") + func unitCurrency() async throws { + #expect(UnitCurrency.baseUnit() == Currencies.find("gp"), "base unit should be goldPieces") let goldPieces = Money(value: 25, unit: Currencies.find("gp")!) let silverPieces = Money(value: 12, unit: Currencies.find("sp")!) @@ -37,154 +35,124 @@ class UnitCurrencyTests: XCTestCase { let totalPieces = goldPieces + silverPieces - copperPieces + electrumPieces - platinumPieces // Should be 25 + 1.2 - 0.01 + 1 - 20 - XCTAssertEqual(totalPieces.value, 7.19, accuracy: 0.0001, "adding coins") + #expect(abs(totalPieces.value - 7.19) < 0.0001, "adding coins should equal 7.19") let totalPiecesInCopper = totalPieces.converted(to: Currencies.find("cp")!) - XCTAssertEqual(totalPiecesInCopper.value, 719, accuracy: 0.01, "adding coins") - + #expect(abs(totalPiecesInCopper.value - 719) < 0.01, "adding coins converted to copper should equal 719") } - func testPrintingValues() { + @Test("Printing currency values") + func printingValues() async throws { let goldPieces = Money(value: 13.7, unit: .baseUnit()) let formatter = MeasurementFormatter() // Test default let gp = formatter.string(from: goldPieces) - XCTAssertEqual(gp, "13.7 gp", "gold pieces") + #expect(gp == "13.7 gp", "gold pieces") // Test provided unit formatter.unitOptions = [.providedUnit] let gpDefault = formatter.string(from: goldPieces) - XCTAssertEqual(gpDefault, "13.7 gp", "gold pieces") + #expect(gpDefault == "13.7 gp", "gold pieces") let silverPieces = goldPieces.converted(to: Currencies.find("sp")!) let sp = formatter.string(from: silverPieces) - XCTAssertEqual(sp, "137 sp", "silver pieces") + #expect(sp == "137 sp", "silver pieces") let platinumPieces = goldPieces.converted(to: Currencies.find("pp")!) let ppProvided = formatter.string(from: platinumPieces) - XCTAssertEqual(ppProvided, "1.37 pp", "platinum pieces") + #expect(ppProvided == "1.37 pp", "platinum pieces") // Test natural scale formatter.unitOptions = [.naturalScale] let ppNatural = formatter.string(from: platinumPieces) - XCTAssertEqual(ppNatural, "13.7 gp", "gold pieces") + #expect(ppNatural == "13.7 gp", "gold pieces") formatter.unitOptions = [.providedUnit] // Test short formatter.unitStyle = .short let gpShort = formatter.string(from: goldPieces) - XCTAssertEqual(gpShort, "13.7gp", "gold pieces") + #expect(gpShort == "13.7gp", "gold pieces") // Test long formatter.unitStyle = .long let gpLong = formatter.string(from: goldPieces) - XCTAssertEqual(gpLong, "13.7 gold pieces", "gold pieces") + #expect(gpLong == "13.7 gold pieces", "gold pieces") let gpSingularLong = formatter.string(from: Money(value: 1.0, unit: .baseUnit())) - XCTAssertEqual(gpSingularLong, "1 gold piece", "gold piece") + #expect(gpSingularLong == "1 gold piece", "gold piece") } - func testMoney() { - do { - let gp = Money(value: 2.5, unit: .baseUnit()) - XCTAssertEqual(gp.value, 2.5, "coinage as Double should be 2.5") - } + @Test("Money parsing and creation") + func money() async throws { + let gp = Money(value: 2.5, unit: .baseUnit()) + #expect(gp.value == 2.5, "coinage as Double should be 2.5") - do { - let cp = "3.2 cp".parseMoney - XCTAssertNotNil(cp, "coinage as cp should not be nil") - if let cp = cp { - XCTAssertEqual(cp.value, 3.2, accuracy: 0.0001, "coinage as string cp should be 3.2") - XCTAssertEqual(cp.unit, Currencies.find("cp"), "coinage as string cp should be copper pieces") - XCTAssertNotEqual(cp.unit, Currencies.find("pp"), "coinage as string cp should not be platinum pieces") - } - } + let cp = "3.2 cp".parseMoney + 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") - do { - let gp = "hello".parseMoney - XCTAssertNil(gp, "coinage as string with hello should be nil") - } + let invalid = "hello".parseMoney + #expect(invalid == nil, "coinage as string with hello should be nil") } - func testMissingCurrenciesFile() { - do { - let bundle = Bundle(for: UnitCurrencyTests.self) + @Test("Missing currencies file") + func missingCurrenciesFile() async throws { + #expect(throws: (any Error).self) { _ = try bundle.loadJSON("Blarg") - XCTFail("load should have thrown an error") - } - catch let error { - XCTAssertTrue(error is ServiceError, "should be a service error") - let description = "\(error)" - XCTAssertTrue(description.contains("Runtime error"), "should be a runtime error") } } - func testDuplicateCurrencies() { - // Try loading the default currencies file a second time. - // It should ignore the duplicate currencies. - do { - XCTAssertEqual(Currencies.allCurrencies.count, 5, "currencies count") - - let bundle = Bundle(for: UnitCurrencyTests.self) - let decoder = JSONDecoder() - let data = try bundle.loadJSON("TestCurrencies") - - _ = try decoder.decode(Currencies.self, from: data) - - XCTAssertEqual(Currencies.allCurrencies.count, 5, "currencies count") - } - catch let error { - XCTFail("duplicate currency should not throw error: \(error)") - } + @Test("Duplicate currencies are ignored") + func duplicateCurrencies() async throws { + #expect(Currencies.allCurrencies.count == 5, "currencies count") + + let data = try bundle.loadJSON("TestCurrencies") + _ = try decoder.decode(Currencies.self, from: data) + + #expect(Currencies.allCurrencies.count == 5, "currencies count should remain 5") } - func testMissingCurrencyTraits() { + @Test("Missing currency traits") + func missingCurrencyTraits() async throws { let decoder = JSONDecoder() // Test missing symbol - do { - let traits = """ - { - "currencies": [{"name": "Foo"}] - } - """.data(using: .utf8)! - let currency = try? decoder.decode(Currencies.self, from: traits) - XCTAssertNil(currency, "missing symbol") + let missingSymbol = """ + { + "currencies": [{"name": "Foo"}] } + """.data(using: .utf8)! + let currencyNoSymbol = try? decoder.decode(Currencies.self, from: missingSymbol) + #expect(currencyNoSymbol == nil, "missing symbol") // Test symbol with missing coefficient - do { - let traits = """ - { - "currencies": [{"symbol": "Foo"}] - } - """.data(using: .utf8)! - - let currency = try? decoder.decode(Currencies.self, from: traits) - XCTAssertNil(currency, "missing coefficient") + let missingCoefficient = """ + { + "currencies": [{"symbol": "Foo"}] } + """.data(using: .utf8)! + let currencyNoCoefficient = try? decoder.decode(Currencies.self, from: missingCoefficient) + #expect(currencyNoCoefficient == nil, "missing coefficient") // Test list of items with missing required traits - do { - let traits = """ - { - "currencies": [{"name": "Foo"}, {"name": "Bar"}] - } - """.data(using: .utf8)! - do { - _ = try decoder.decode(Currencies.self, from: traits) - XCTFail("should have thrown an error") - } - catch let error { - print("Successfully caught error decoding missing required traits for Currencies. Error: \(error)") - } + let missingTraits = """ + { + "currencies": [{"name": "Foo"}, {"name": "Bar"}] + } + """.data(using: .utf8)! + + #expect(throws: (any Error).self) { + _ = try decoder.decode(Currencies.self, from: missingTraits) } } - func testEncodingMoney() { + @Test("Encoding money") + func encodingMoney() async throws { struct MoneyContainer: Encodable { let money: Money @@ -198,133 +166,99 @@ class UnitCurrencyTests: XCTestCase { } } - do { - let moneyContainer = MoneyContainer(money: Money(value: 48.93, unit: Currencies.find("sp")!)) - let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(moneyContainer) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] - XCTAssertEqual(deserialized?["money"], "48.93 sp", "encoded money failed to deserialize as string") - } - catch let error { - XCTFail("encoded dice failed, error: \(error)") - } - } + let moneyContainer = MoneyContainer(money: Money(value: 48.93, unit: Currencies.find("sp")!)) + let encoder = JSONEncoder() + let encoded = try encoder.encode(moneyContainer) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] + + #expect(deserialized?["money"] == "48.93 sp", "encoded money failed to deserialize as string") } - func testDecodingMoney() { + @Test("Decoding money from string and number") + func decodingMoney() async throws { struct MoneyContainer: Decodable { let money: Money - } let decoder = JSONDecoder() // Test parseable string - do { - let traits = """ - { - "money": "72.17 ep" - } - """.data(using: .utf8)! - let moneyContainer = try decoder.decode(MoneyContainer.self, from: traits) - XCTAssertEqual("\(moneyContainer.money)", "72.17 ep", "Decoded money") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") + let stringMoney = """ + { + "money": "72.17 ep" } + """.data(using: .utf8)! + let stringContainer = try decoder.decode(MoneyContainer.self, from: stringMoney) + #expect("\(stringContainer.money)" == "72.17 ep", "Decoded money from string") // Test raw number - do { - let traits = """ - { - "money": 85 - } - """.data(using: .utf8)! - let moneyContainer = try decoder.decode(MoneyContainer.self, from: traits) - XCTAssertEqual("\(moneyContainer.money)", "85.0 gp", "Decoded money") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") + let numberMoney = """ + { + "money": 85 } + """.data(using: .utf8)! + let numberContainer = try decoder.decode(MoneyContainer.self, from: numberMoney) + #expect("\(numberContainer.money)" == "85.0 gp", "Decoded money from number") // Test invalid value - do { - let traits = """ - { - "money": "no money" - } - """.data(using: .utf8)! - _ = try decoder.decode(MoneyContainer.self, from: traits) - XCTFail("decoded dice should have failed") - + let invalidMoney = """ + { + "money": "no money" } - catch let error { - print("Successfully failed to decode invalid dice string, error: \(error)") + """.data(using: .utf8)! + + #expect(throws: (any Error).self) { + _ = try decoder.decode(MoneyContainer.self, from: invalidMoney) } } - func testDecodingMoneyIfPresent() { + @Test("Decoding optional money") + func decodingMoneyIfPresent() async throws { struct MoneyContainer: Decodable { let money: Money? - } let decoder = JSONDecoder() // Test parseable string - do { - let traits = """ - { - "money": "72.17 ep" - } - """.data(using: .utf8)! - let moneyContainer = try decoder.decode(MoneyContainer.self, from: traits) - XCTAssertEqual("\(moneyContainer.money!)", "72.17 ep", "Decoded money") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") + let stringMoney = """ + { + "money": "72.17 ep" } + """.data(using: .utf8)! + let stringContainer = try decoder.decode(MoneyContainer.self, from: stringMoney) + #expect("\(stringContainer.money!)" == "72.17 ep", "Decoded money from string") // Test raw number - do { - let traits = """ - { - "money": 85 - } - """.data(using: .utf8)! - let moneyContainer = try decoder.decode(MoneyContainer.self, from: traits) - XCTAssertEqual("\(moneyContainer.money!)", "85.0 gp", "Decoded money") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") + let numberMoney = """ + { + "money": 85 } + """.data(using: .utf8)! + let numberContainer = try decoder.decode(MoneyContainer.self, from: numberMoney) + #expect("\(numberContainer.money!)" == "85.0 gp", "Decoded money from number") - // Test invalid value - do { - let traits = """ - { - "money": "no money" - } - """.data(using: .utf8)! - let moneyContainer = try decoder.decode(MoneyContainer.self, from: traits) - XCTAssertNil(moneyContainer.money, "decoded dice should have failed") - } - catch let error { - XCTFail("Failed to decode optional invalid dice string as nil, error: \(error)") + // Test invalid value should result in nil for optional + let invalidMoney = """ + { + "money": "no money" } + """.data(using: .utf8)! + let invalidContainer = try decoder.decode(MoneyContainer.self, from: invalidMoney) + #expect(invalidContainer.money == nil, "decoded invalid money string should be nil") } - func testEncodeCurrencies() { + @Test("Encoding currencies") + func encodeCurrencies() async throws { let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(UnitCurrencyTests.currencies) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: Any] - XCTAssertNotNil(deserialized) - let currencies = deserialized?["currencies"] as? [[String:Any]] - XCTAssertNotNil(currencies) - XCTAssertEqual(currencies?.count, 5, "5 currencies") - } - catch let error { - XCTFail("Failed to encode currencies, error: \(error)") - } + let encoded = try encoder.encode(currencies) + let deserialized = try #require( + JSONSerialization.jsonObject(with: encoded, options: []) as? [String: Any], + "Failed to deserialize encoded currencies" + ) + + let currenciesArray = try #require( + deserialized["currencies"] as? [[String: Any]], + "Failed to get currencies array" + ) + #expect(currenciesArray.count == 5, "5 currencies") } } diff --git a/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift b/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift index 6dd9dc1..9bdc7f3 100644 --- a/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift @@ -13,412 +13,305 @@ import RolePlayingCore class DiceParserTests: XCTestCase { func testDiceFormatString() { - // Test dice - do { - let formatString = "d12" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((1...12).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((6.0...7.0).contains(mean), "expected mean around 6.5, got \(mean)") - - XCTAssertEqual(minValue, 1, "min value") - XCTAssertEqual(maxValue, 12, "max value") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 6.5]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") - } + let formatString = "d12" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((1...12).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((6.0...7.0).contains(mean), "expected mean around 6.5, got \(mean)") - // Test times - do { - let formatString = "2d10" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((2...20).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") - - // TODO: Because 2d10 produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 3, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 19, "max value") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 11.0]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") - } + XCTAssertEqual(minValue, 1, "min value") + XCTAssertEqual(maxValue, 12, "max value") + } + + func testDiceTimesString() { + let formatString = "2d10" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((2...20).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") - // Test times with capital "D" - do { - let formatString = "2D10" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((2...20).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") - - // TODO: Because 2d10 produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 3, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 19, "max value") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 11.0]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") - } + // TODO: Because 2d10 produces a bell curve, the actual min/max may be harder to get in a sample + XCTAssertLessThanOrEqual(minValue, 3, "min value") + XCTAssertGreaterThanOrEqual(maxValue, 19, "max value") + } + + func testDiceTimesCapitalized() { + let formatString = "2D10" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((2...20).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") - // Test add modifier - do { - let formatString = "1d20+4" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((5...24).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") - - XCTAssertEqual(minValue, 5, "min value") - XCTAssertEqual(maxValue, 24, "max value") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 14.5]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") - } + // TODO: Because 2d10 produces a bell curve, the actual min/max may be harder to get in a sample + XCTAssertLessThanOrEqual(minValue, 3, "min value") + XCTAssertGreaterThanOrEqual(maxValue, 19, "max value") + } + + func testDiceAddModifier() { + let formatString = "1d20+4" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((5...24).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") - // Test percent - do { - let formatString = "d%" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((1...100).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((45.0...56.0).contains(mean), "expected mean around 50.5, got \(mean)") - - /// With such a big range, we may not hit the absolute min/max for the specified sample size. - XCTAssertLessThanOrEqual(minValue, 2, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 99, "max value") - - // Check that the description has the % - if formatDice != nil { - XCTAssertEqual("\(formatDice!.description)", "d%", "% description") - } - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 50.5]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") - } + XCTAssertEqual(minValue, 5, "min value") + XCTAssertEqual(maxValue, 24, "max value") + } - // Test multiply - do { - let formatString = "2d4x10" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((20...80).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") - - XCTAssertEqual(minValue, 20, "min value") - XCTAssertEqual(maxValue, 80, "max value") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 50.0]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") - } + func testDicePercent() { + let formatString = "d%" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((1...100).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((45.0...56.0).contains(mean), "expected mean around 50.5, got \(mean)") - // Test multiply with '*' - do { - let formatString = "2d4*10" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((20...80).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") - - XCTAssertEqual(minValue, 20, "min value") - XCTAssertEqual(maxValue, 80, "max value") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 50.0]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") + /// With such a big range, we may not hit the absolute min/max for the specified sample size. + XCTAssertLessThanOrEqual(minValue, 2, "min value") + XCTAssertGreaterThanOrEqual(maxValue, 99, "max value") + + // Check that the description has the % + if formatDice != nil { + XCTAssertEqual("\(formatDice!.description)", "d%", "% description") } + } - // Test divide - do { - let formatString = "d100/10" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((0...10).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((4.0...5.0).contains(mean), "expected mean around 4.5, got \(mean)") - - XCTAssertGreaterThanOrEqual(minValue, 0, "min value") - XCTAssertLessThanOrEqual(maxValue, 10, "max value") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 4.5]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") + func testMultiplyWithX() { + let formatString = "2d4x10" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((20...80).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") + + XCTAssertEqual(minValue, 20, "min value") + XCTAssertEqual(maxValue, 80, "max value") + } + + func testMultiplyWithAsterisk() { + let formatString = "2d4*10" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((20...80).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") + + XCTAssertEqual(minValue, 20, "min value") + XCTAssertEqual(maxValue, 80, "max value") + } + + func testDivide() { + let formatString = "d100/10" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((0...10).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((4.0...5.0).contains(mean), "expected mean around 4.5, got \(mean)") + + XCTAssertGreaterThanOrEqual(minValue, 0, "min value") + XCTAssertLessThanOrEqual(maxValue, 10, "max value") + } + + func testDroppingLowest() { + let formatString = "4d6-L" + + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((3...18).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) } - // Test dropping "L" - do { - let formatString = "4d6-L" - print("Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((3...18).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((11.0...13.5).contains(mean), "expected mean around 12.25, got \(mean)") - - // TODO: Because 4x-L produces a sharp bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 5, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 16, "max value") - - XCTAssertEqual(formatDice?.sides, 6, "Dice sides") - if let formatDice = formatDice { - XCTAssertEqual("\(formatDice.description)", "4d6-L", "SimpleDice description") - } - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 12.25]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((11.0...13.5).contains(mean), "expected mean around 12.25, got \(mean)") + + // TODO: Because 4x-L produces a sharp bell curve, the actual min/max may be harder to get in a sample + XCTAssertLessThanOrEqual(minValue, 5, "min value") + XCTAssertGreaterThanOrEqual(maxValue, 16, "max value") + + XCTAssertEqual(formatDice?.sides, 6, "Dice sides") + if let formatDice = formatDice { + XCTAssertEqual("\(formatDice.description)", "4d6-L", "SimpleDice description") } } func testComplexDiceFormatString() { - do { - let formatString = "2d4+3d12-4" - print("Complex Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((1...40).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((19.0...22.0).contains(mean), "expected mean around 20.5, got \(mean)") - - // TODO: Because this produces a sharp bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 7, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 34, "max value") - - XCTAssertEqual(formatDice?.sides, 4, "Dice sides") - if formatDice != nil { - XCTAssertEqual("\(formatDice!.description)", "2d4+3d12-4", "SimpleDice description") - } - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 20.5]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\"") + let formatString = "2d4+3d12-4" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((1...40).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) } - // Test with subtraction second to last, to ensure operator precedence - do { - let formatString = "2d4+d12-2+5" - print("Complex Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - - var sum = 0 - var minValue = 0 - var maxValue = 0 - var lastRoll = 0 - - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((6...23).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - - lastRoll = roll - } - - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") - - // TODO: Because this produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 7, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 22, "max value") - - XCTAssertEqual(formatDice?.sides, 4, "Dice sides") - if formatDice != nil { - XCTAssertEqual("\(formatDice!.description)", "2d4+d12-2+5", "SimpleDice description") - } - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 14.5]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\" = \(lastRoll)") + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((19.0...22.0).contains(mean), "expected mean around 20.5, got \(mean)") + + // TODO: Because this produces a sharp bell curve, the actual min/max may be harder to get in a sample + XCTAssertLessThanOrEqual(minValue, 7, "min value") + XCTAssertGreaterThanOrEqual(maxValue, 34, "max value") + + XCTAssertEqual(formatDice?.sides, 4, "Dice sides") + if formatDice != nil { + XCTAssertEqual("\(formatDice!.description)", "2d4+3d12-4", "SimpleDice description") } + } + + func testComplexDiceOperatorPrecedence() { + let formatString = "2d4+d12-2+5" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - // Repeat with extra roll, dropping, spaces and returns - do { - let formatString = "3d4- L + d12 -\n2 + 5" - print("Complex Format Dice \(formatString):") - - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") - - var sum = 0 - var minValue = 0 - var maxValue = 0 - var lastRoll = 0 - - for _ in 0 ..< sampleSize { - let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((6...23).contains(roll), "rolling \(formatString), got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - - lastRoll = roll - } - - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") - - // TODO: Because this produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 7, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 22, "max value") - - XCTAssertEqual(formatDice?.sides, 4, "Dice sides") - if formatDice != nil { - XCTAssertEqual("\(formatDice!.description)", "3d4-L+d12-2+5", "SimpleDice description") - } - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 14.5]") - let result = formatDice?.roll().description ?? "" - print(" lastRoll \"\(result)\" = \(lastRoll)") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((6...23).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) } - // Test two constant modifiers - do { - let formatString = "1+3" - let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should not be nil") - - if let formatDice = formatDice { - XCTAssertEqual(formatDice.description, "1+3", "format string") - let lastRoll = formatDice.roll() - XCTAssertEqual(lastRoll.description, "1 + 3", "format string") - } + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") + + // TODO: Because this produces a bell curve, the actual min/max may be harder to get in a sample + XCTAssertLessThanOrEqual(minValue, 7, "min value") + XCTAssertGreaterThanOrEqual(maxValue, 22, "max value") + + XCTAssertEqual(formatDice?.sides, 4, "Dice sides") + if formatDice != nil { + XCTAssertEqual("\(formatDice!.description)", "2d4+d12-2+5", "SimpleDice description") + } + } + + func testComplexDiceExtraRollDroppingWithWhitespace() { + let formatString = "3d4- L + d12 -\n2 + 5" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = formatDice?.roll().result ?? 0 + XCTAssertTrue((6...23).contains(roll), "rolling \(formatString), got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + + let mean = Double(sum)/Double(sampleSize) + XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") + + // TODO: Because this produces a bell curve, the actual min/max may be harder to get in a sample + XCTAssertLessThanOrEqual(minValue, 7, "min value") + XCTAssertGreaterThanOrEqual(maxValue, 22, "max value") + + XCTAssertEqual(formatDice?.sides, 4, "Dice sides") + if formatDice != nil { + XCTAssertEqual("\(formatDice!.description)", "3d4-L+d12-2+5", "SimpleDice description") + } + } + + func testConstantModifiers() { + let formatString = "1+3" + let formatDice = formatString.parseDice + XCTAssertNotNil(formatDice, "Dice from \(formatString) should not be nil") + + if let formatDice = formatDice { + XCTAssertEqual(formatDice.description, "1+3", "format string") + let lastRoll = formatDice.roll() + XCTAssertEqual(lastRoll.description, "1 + 3", "format string") } } @@ -562,8 +455,8 @@ class DiceParserTests: XCTestCase { _ = try decoder.decode(DiceContainer.self, from: traits) XCTFail("decode invalid dice string should have failed") } - catch let error { - print("successfully failed to decode invalid dice string, error: \(error)") + catch { + // Successfully errored } } } @@ -637,30 +530,28 @@ class DiceParserTests: XCTestCase { } func testEncodingDice() { - do { - struct DiceContainer: Encodable { - let dice: Dice - - enum CodingKeys: String, CodingKey { - case dice - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode("\(dice)", forKey: .dice) - } - } + struct DiceContainer: Encodable { + let dice: Dice - let diceContainer = DiceContainer(dice: CompoundDice(.d8, times: 3, modifier: 3, mathOperator: "-")) - let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(diceContainer) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] - XCTAssertEqual(deserialized?["dice"], "3d8-3", "encoded dice failed to deserialize as string") + enum CodingKeys: String, CodingKey { + case dice } - catch let error { - XCTFail("encoded dice failed, error: \(error)") + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("\(dice)", forKey: .dice) } } + + let diceContainer = DiceContainer(dice: CompoundDice(.d8, times: 3, modifier: 3, mathOperator: "-")) + let encoder = JSONEncoder() + do { + let encoded = try encoder.encode(diceContainer) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] + XCTAssertEqual(deserialized?["dice"], "3d8-3", "encoded dice failed to deserialize as string") + } + catch let error { + XCTFail("encoded dice failed, error: \(error)") + } } } diff --git a/RolePlayingCore/RolePlayingCoreTests/DiceTests.swift b/RolePlayingCore/RolePlayingCoreTests/DiceTests.swift index ed873f1..89b9e1b 100644 --- a/RolePlayingCore/RolePlayingCoreTests/DiceTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/DiceTests.swift @@ -6,8 +6,7 @@ // Copyright © 2016-2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore /// Use a sample size large enough to hit relatively tight ranges of @@ -19,38 +18,37 @@ let sampleSize = 1024 /// - Tolerance may be wide enough in some cases that they may not catch all regressions (false positives) /// - Once in a blue moon, tests may fail just outside of the tolerance (false negatives) /// -/// As modest countermeasure, print statements were added to the test output, so that -/// manual inspection can be performed, to help determine what might be happenening. - -class DiceTests: XCTestCase { +@Suite("Dice Tests") +struct DiceTests { - func testCreateDie() { + @Test("Create die") + func createDie() { // Test raw value creation matches enums let d4 = Die(rawValue: 4) - XCTAssertEqual(d4, Die.d4, "d4") + #expect(d4 == Die.d4, "d4") let d6 = Die(rawValue: 6) - XCTAssertEqual(d6, Die.d6, "d6") + #expect(d6 == Die.d6, "d6") let d8 = Die(rawValue: 8) - XCTAssertEqual(d8, Die.d8, "d8") + #expect(d8 == Die.d8, "d8") let d10 = Die(rawValue: 10) - XCTAssertEqual(d10, Die.d10, "d10") + #expect(d10 == Die.d10, "d10") let d12 = Die(rawValue: 12) - XCTAssertEqual(d12, Die.d12, "d12") + #expect(d12 == Die.d12, "d12") let d20 = Die(rawValue: 20) - XCTAssertEqual(d20, Die.d20, "d20") + #expect(d20 == Die.d20, "d20") let d100 = Die(rawValue: 100) - XCTAssertEqual(d100, Die.d100, "Dice %") + #expect(d100 == Die.d100, "Dice %") } - func testCreateDieNegative() { + @Test("Create die negative") + func createDieNegative() { // Negative tests: bad raw values and strings let badDie = Die(rawValue: 7) - XCTAssertNil(badDie, "d7 should be nil") + #expect(badDie == nil, "d7 should be nil") } - func testRollDie() { - print("Die d4:") - // Test rolling 1 time with d4 + @Test("Roll die") + func rollDie() { let die: Die = .d4 var sum = 0 @@ -58,226 +56,181 @@ class DiceTests: XCTestCase { var maxValue = 0 for _ in 0 ..< sampleSize { let roll = die.roll() - XCTAssertTrue((1...4).contains(roll), "rolling d4, got \(roll)") + #expect((1...4).contains(roll), "rolling d4, got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((2.0...3.0).contains(mean), "expected mean around 2.5, got \(mean)") - - XCTAssertEqual(minValue, 1, "min value") - XCTAssertEqual(maxValue, 4, "max value") + #expect((2.0...3.0).contains(mean), "expected mean around 2.5, got \(mean)") - XCTAssertEqual(Die.d4.description, "d4", "d4 description") + #expect(minValue == 1, "min value") + #expect(maxValue == 4, "max value") - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 2.5]") + #expect(Die.d4.description == "d4", "d4 description") } - func testDiceModifier() { + @Test("Dice modifier") + func diceModifier() { let diceModifier = DiceModifier(7) let diceRoll = diceModifier.roll() - XCTAssertEqual(diceModifier.modifier, 7, "dice modifier value") - XCTAssertEqual(diceRoll.result, 7, "dice modifier roll") - XCTAssertEqual(diceModifier.sides, 7, "dice modifier sides") - XCTAssertEqual(diceModifier.description, "7", "dice modifier description") - XCTAssertEqual(diceRoll.description, "7", "dice modifier lastRollDescription") + #expect(diceModifier.modifier == 7, "dice modifier value") + #expect(diceRoll.result == 7, "dice modifier roll") + #expect(diceModifier.sides == 7, "dice modifier sides") + #expect(diceModifier.description == "7", "dice modifier description") + #expect(diceRoll.description == "7", "dice modifier lastRollDescription") } - func testSimpleDice() { - // Test d12 - do { - print("SimpleDice d12:") - let simpleDice = SimpleDice(.d12) - - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = simpleDice.roll().result - XCTAssertTrue((1...12).contains(roll), "rolling d12, got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((6.0...7.0).contains(mean), "expected mean around 6.5, got \(mean)") - - // TODO: Because 2x produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 1, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 12, "max value") - - XCTAssertEqual(simpleDice.sides, 12, "SimpleDice sides") - XCTAssertEqual("\(simpleDice.description)", "d12", "SimpleDice description") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 6.5]") - let lastRoll = simpleDice.roll() - print(" lastRoll = \"\(lastRoll.description)\"") + @Test("Simple dice d12") + func simpleDiceD12() { + let simpleDice = SimpleDice(.d12) + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = simpleDice.roll().result + #expect((1...12).contains(roll), "rolling d12, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) } + let mean = Double(sum)/Double(sampleSize) + #expect((6.0...7.0).contains(mean), "expected mean around 6.5, got \(mean)") + + // TODO: Because 2x produces a bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 1, "min value") + #expect(maxValue >= 12, "max value") - // Test 2d8 - do { - print("SimpleDice 2d8:") - let simpleDice = SimpleDice(.d8, times: 2) - - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = simpleDice.roll().result - XCTAssertTrue((2...16).contains(roll), "rolling 2d8, got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((7.5...9.5).contains(mean), "expected mean around 8.5, got \(mean)") - - // TODO: Because 2x produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertEqual(minValue, 2, "min value") - XCTAssertEqual(maxValue, 16, "max value") - - XCTAssertEqual(simpleDice.sides, 8, "SimpleDice sides") - XCTAssertEqual("\(simpleDice.description)", "2d8", "SimpleDice description") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 8.5]") - let lastRoll = simpleDice.roll() - print(" lastRoll = \"\(lastRoll.description)\"") + #expect(simpleDice.sides == 12, "SimpleDice sides") + #expect("\(simpleDice.description)" == "d12", "SimpleDice description") + } + + @Test("Simple dice 2d8") + func testSimpleDice2d8() { + let simpleDice = SimpleDice(.d8, times: 2) + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = simpleDice.roll().result + #expect((2...16).contains(roll), "rolling 2d8, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) } + let mean = Double(sum)/Double(sampleSize) + #expect((7.5...9.5).contains(mean), "expected mean around 8.5, got \(mean)") + + // TODO: Because 2x produces a bell curve, the actual min/max may be harder to get in a sample + #expect(minValue == 2, "min value") + #expect(maxValue == 16, "max value") + + #expect(simpleDice.sides == 8, "SimpleDice sides") + #expect("\(simpleDice.description)" == "2d8", "SimpleDice description") } - func testDroppingDice() { - // Test 4d6, dropping the lowest - do { - print("SimpleDice 4d6-L:") - let simpleDice = DroppingDice(.d6, times: 4, drop: .lowest) + @Test("Dropping dice lowest") + func droppingDiceLowest() { + let simpleDice = DroppingDice(.d6, times: 4, drop: .lowest) - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = simpleDice.roll().result - XCTAssertTrue((3...18).contains(roll), "rolling 4d6-L, got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((11.0...13.5).contains(mean), "expected mean around 12.25, got \(mean)") - - // TODO: Because 4x-L produces a sharp bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 5, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 16, "max value") - - XCTAssertEqual(simpleDice.sides, 6, "SimpleDice sides") - XCTAssertEqual("\(simpleDice.description)", "4d6-L", "SimpleDice description") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 12.25]") - let lastRoll = simpleDice.roll() - print(" lastRoll = \"\(lastRoll.description)\"") - - // TODO: verify that it is actually dropping the lowest score. + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = simpleDice.roll().result + #expect((3...18).contains(roll), "rolling 4d6-L, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) } + let mean = Double(sum)/Double(sampleSize) + #expect((11.0...13.5).contains(mean), "expected mean around 12.25, got \(mean)") - // Test 3d4, dropping the highest - do { - print("SimpleDice 3d4-H:") - let simpleDice = DroppingDice(.d4, times: 3, drop: .highest) - - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = simpleDice.roll().result - XCTAssertTrue((2...8).contains(roll), "rolling 3d4-H, got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((3.7...4.3).contains(mean), "expected mean around 4, got \(mean)") - - XCTAssertEqual(minValue, 2, "min value") - XCTAssertEqual(maxValue, 8, "max value") - - XCTAssertEqual(simpleDice.sides, 4, "SimpleDice sides") - XCTAssertEqual("\(simpleDice.description)", "3d4-H", "SimpleDice description") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 4]") - let lastRoll = simpleDice.roll() - print(" lastRoll = \"\(lastRoll.description)\"") + // TODO: Because 4x-L produces a sharp bell curve, the actual min/max may be harder to get in a sample + #expect(minValue <= 5, "min value") + #expect(maxValue >= 16, "max value") + + #expect(simpleDice.sides == 6, "SimpleDice sides") + #expect("\(simpleDice.description)" == "4d6-L", "SimpleDice description") - // TODO: verify that it is actually dropping the highest score. + // TODO: verify that it is actually dropping the lowest score. + } + + @Test("Dropping dice highest") + func droppingDiceHighest() { + let simpleDice = DroppingDice(.d4, times: 3, drop: .highest) + + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = simpleDice.roll().result + #expect((2...8).contains(roll), "rolling 3d4-H, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) } + let mean = Double(sum)/Double(sampleSize) + #expect((3.7...4.3).contains(mean), "expected mean around 4, got \(mean)") + + #expect(minValue == 2, "min value") + #expect(maxValue == 8, "max value") + + #expect(simpleDice.sides == 4, "SimpleDice sides") + #expect("\(simpleDice.description)" == "3d4-H", "SimpleDice description") + + // TODO: verify that it is actually dropping the highest score. } - func testCompoundDice() { - // Test 2d8+4 - do { - print("CompoundDice 2d8+4:") + @Test("Compound dice with modifier") + func compoundDiceWithModifier() { + let compoundDice = CompoundDice(.d8, times: 2, modifier: 4) - let compoundDice = CompoundDice(.d8, times: 2, modifier: 4) - - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = compoundDice.roll().result - XCTAssertTrue((6...20).contains(roll), "rolling 2d8+4, got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((12.0...14.0).contains(mean), "expected mean around 13.0, got \(mean)") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = compoundDice.roll().result + #expect((6...20).contains(roll), "rolling 2d8+4, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) + } + let mean = Double(sum)/Double(sampleSize) + #expect((12.0...14.0).contains(mean), "expected mean around 13.0, got \(mean)") - XCTAssertEqual(minValue, 6, "min value") - XCTAssertEqual(maxValue, 20, "max value") - - XCTAssertEqual(compoundDice.sides, 8, "CompoundDice sides") + #expect(minValue == 6, "min value") + #expect(maxValue == 20, "max value") + + #expect(compoundDice.sides == 8, "CompoundDice sides") - XCTAssertEqual("\(compoundDice.description)", "2d8+4", "CompoundDice description") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 13.0]") - let lastRoll = compoundDice.roll() - print(" lastRoll = \"\(lastRoll.description)\"") - } + #expect("\(compoundDice.description)" == "2d8+4", "CompoundDice description") + } - // Test 2d8+d4 - do { - print("CompoundDice 2d8+d4:") - let compoundDice = CompoundDice(lhs: SimpleDice(.d8, times: 2), rhs: SimpleDice(.d4), mathOperator: "+") - var sum = 0 - var minValue = 0 - var maxValue = 0 - for _ in 0 ..< sampleSize { - let roll = compoundDice.roll().result - XCTAssertTrue((3...20).contains(roll), "rolling 2d8+d4, got \(roll)") - sum += roll - minValue = minValue == 0 ? roll : min(minValue, roll) - maxValue = maxValue == 0 ? roll : max(maxValue, roll) - } - let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((11.0...12.0).contains(mean), "expected mean around 11.5, got \(mean)") - - XCTAssertLessThanOrEqual(minValue, 4, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 19, "max value") - - XCTAssertEqual(compoundDice.sides, 8, "CompoundDice sides") - XCTAssertEqual("\(compoundDice.description)", "2d8+d4", "CompoundDice description") - - // Code coverage and manual inspection of test output: - print(" mean = \(mean) [expect 11.5]") - let lastRoll = compoundDice.roll() - print(" lastRoll = \"\(lastRoll.description)\"") + @Test("Compound dice with dice") + func compoundDiceWithDice() { + let compoundDice = CompoundDice(lhs: SimpleDice(.d8, times: 2), rhs: SimpleDice(.d4), mathOperator: "+") + var sum = 0 + var minValue = 0 + var maxValue = 0 + for _ in 0 ..< sampleSize { + let roll = compoundDice.roll().result + #expect((3...20).contains(roll), "rolling 2d8+d4, got \(roll)") + sum += roll + minValue = minValue == 0 ? roll : min(minValue, roll) + maxValue = maxValue == 0 ? roll : max(maxValue, roll) } + let mean = Double(sum)/Double(sampleSize) + #expect((11.0...12.0).contains(mean), "expected mean around 11.5, got \(mean)") + + #expect(minValue <= 4, "min value") + #expect(maxValue >= 19, "max value") + + #expect(compoundDice.sides == 8, "CompoundDice sides") + #expect("\(compoundDice.description)" == "2d8+d4", "CompoundDice description") } } diff --git a/RolePlayingCore/RolePlayingCoreTests/HeightTests.swift b/RolePlayingCore/RolePlayingCoreTests/HeightTests.swift index 54496b2..aa92221 100644 --- a/RolePlayingCore/RolePlayingCoreTests/HeightTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/HeightTests.swift @@ -6,63 +6,66 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class UnitHeightTests: XCTestCase { +@Suite("Height Parsing and Serialization Tests") +struct UnitHeightTests { - func testHeights() { + @Test("Parse various height formats") + func heights() async throws { do { let howTall = "5".parseHeight - XCTAssertNotNil(howTall, "height should be non-nil") - XCTAssertEqual(howTall?.value, 5.0, "height should be 3.0") + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 5.0, "height should be 5.0") } do { let howTall = "3.0".parseHeight - XCTAssertNotNil(howTall, "height should be non-nil") - XCTAssertEqual(howTall?.value, 3.0, "height should be 3.0") + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 3.0, "height should be 3.0") } do { let howTall = "4 ft 3 in".parseHeight - XCTAssertNotNil(howTall, "height should be non-nil") - XCTAssertEqual(howTall?.value, 4.0 + 3.0/12.0, "height should be 4.25") + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 4.0 + 3.0/12.0, "height should be 4.25") } do { let howTall = "73in".parseHeight - XCTAssertNotNil(howTall, "height should be non-nil") + #expect(howTall != nil, "height should be non-nil") let howTallValue = howTall?.value ?? 0.0 - XCTAssertEqual(howTallValue, 6.0 + 1.0/12.0, accuracy: 0.0001, "height should be 6.08") + #expect(abs(howTallValue - (6.0 + 1.0/12.0)) < 0.0001, "height should be 6.08") } do { let howTall = "5'4\"".parseHeight - XCTAssertNotNil(howTall, "height should be non-nil") - XCTAssertEqual(howTall?.value, 5.0 + 4.0/12.0, "height should be 5.33") + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 5.0 + 4.0/12.0, "height should be 5.33") } do { let howTall = "130 cm".parseHeight?.converted(to: .meters) - XCTAssertNotNil(howTall, "height should be non-nil") - XCTAssertEqual(howTall?.value, 1.3, "height should be 1.3") + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 1.3, "height should be 1.3") } do { let howTall = "2.1m".parseHeight - XCTAssertNotNil(howTall, "height should be non-nil") - XCTAssertEqual(howTall?.value, 2.1, "height should be 2.1") + #expect(howTall != nil, "height should be non-nil") + #expect(howTall?.value == 2.1, "height should be 2.1") } } - func testInvalidHeights() { + @Test("Parse invalid height strings") + func invalidHeights() async throws { let howTall = "3 hello".parseHeight - XCTAssertNil(howTall, "height should be nil") + #expect(howTall == nil, "height should be nil") } - func testEncodingHeight() { + @Test("Encode height to JSON") + func encodingHeight() async throws { struct HeightContainer: Encodable { let height: Height @@ -78,21 +81,17 @@ class UnitHeightTests: XCTestCase { let heightContainer = HeightContainer(height: Height(value: 3.2, unit: .meters)) // Do a round-trip through serialization, then deserialization to confirm that it became a string - do { - let encoder = JSONEncoder() - let encoded = try encoder.encode(heightContainer) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) - print("deserialized = \n\(deserialized)") - let container = deserialized as? [String: Any] - let height = container?["height"] as? String - XCTAssertEqual(height, "3.2 m", "Encoded height did not become a string") - } - catch let error { - XCTFail("Encoding heights threw an error: \(error)") - } + let encoder = JSONEncoder() + let encoded = try encoder.encode(heightContainer) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) + print("deserialized = \n\(deserialized)") + let container = deserialized as? [String: Any] + let height = container?["height"] as? String + #expect(height == "3.2 m", "Encoded height did not become a string") } - func testDecodingHeight() { + @Test("Decode height from JSON") + func decodingHeight() async throws { struct HeightContainer: Decodable { let height: Height } @@ -104,14 +103,11 @@ class UnitHeightTests: XCTestCase { "height": "4ft 3in" } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(HeightContainer.self, from: traits) - - XCTAssertEqual(decoded.height.value, 4.25, "Decoded height should be 4 ft 3 in") - } catch let error { - XCTFail("Decoding heights threw an error: \(error)") - } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(HeightContainer.self, from: traits) + + #expect(decoded.height.value == 4.25, "Decoded height should be 4 ft 3 in") } // Test decoding from double height @@ -121,36 +117,30 @@ class UnitHeightTests: XCTestCase { "height": 6.5 } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(HeightContainer.self, from: traits) - - XCTAssertEqual(decoded.height.value, 6.5, "Decoded height should be 4 ft 3 in") - } catch let error { - XCTFail("Decoding height threw an error: \(error)") - } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(HeightContainer.self, from: traits) + + #expect(decoded.height.value == 6.5, "Decoded height should be 6.5") } // Test failure to decode - // Test decoding from double height do { let traits = """ { "height": "abcdefg" } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() + + let decoder = JSONDecoder() + #expect(throws: (any Error).self) { _ = try decoder.decode(HeightContainer.self, from: traits) - XCTFail("Decoding height should have thrown an error") - - } catch let error { - print("Decoding invalid height successfully threw an error: \(error)") } } } - func testDecodingHeightIfPresent() { + @Test("Decode optional height from JSON") + func decodingHeightIfPresent() async throws { struct HeightContainer: Decodable { let height: Height? // The ? will trigger decodeIfPresent in the decoder } @@ -162,17 +152,13 @@ class UnitHeightTests: XCTestCase { "height": "4ft 3in" } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(HeightContainer.self, from: traits) - - XCTAssertEqual(decoded.height?.value, 4.25, "Decoded height should be 4 ft 3 in") - } catch let error { - XCTFail("Decoding heights threw an error: \(error)") - } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(HeightContainer.self, from: traits) + + #expect(decoded.height?.value == 4.25, "Decoded height should be 4 ft 3 in") } - // Test decoding from double height do { let traits = """ @@ -180,14 +166,11 @@ class UnitHeightTests: XCTestCase { "height": 6.5 } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(HeightContainer.self, from: traits) - - XCTAssertEqual(decoded.height?.value, 6.5, "Decoded height should be 4 ft 3 in") - } catch let error { - XCTFail("Decoding height threw an error: \(error)") - } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(HeightContainer.self, from: traits) + + #expect(decoded.height?.value == 6.5, "Decoded height should be 6.5") } } } diff --git a/RolePlayingCore/RolePlayingCoreTests/JSONFileTests.swift b/RolePlayingCore/RolePlayingCoreTests/JSONFileTests.swift index 01efffb..e9feb10 100644 --- a/RolePlayingCore/RolePlayingCoreTests/JSONFileTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/JSONFileTests.swift @@ -6,8 +6,7 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore struct JSONFileData: Codable { @@ -25,76 +24,72 @@ struct AnyFileData: Codable { // No-op } -class JSONFileTests: XCTestCase { +@Suite("JSON File Loading Tests") +struct JSONFileTests { let decoder = JSONDecoder() - func testJSON() { + @Test("Load and parse valid JSON file") + func json() async throws { // This test file has all of the basic elements of a JSON file - let bundle = Bundle(for: JSONFileTests.self) - do { - let jsonData = try bundle.loadJSON("JSONFile") - let jsonObject = try decoder.decode(JSONFileData.self, from: jsonData) - - // Test for contents of JSON file - - let bool = jsonObject.boolValue - XCTAssertNotNil(bool, "bool should be non-nil") - XCTAssertEqual(bool, true, "bool should be true") - - let dictionary = jsonObject.dictionaryValue - - let string = dictionary.stringValue - XCTAssertEqual(string, "foo", "string should be \"foo\"") - - let double = dictionary.doubleValue - XCTAssertEqual(double, 2.1, "double should be 2.1") - - let array = dictionary.arrayValue - XCTAssertEqual(array, [2, 3], "array should be [2, 3]") - } - catch let error { - XCTFail("loadJSON should not throw an error: \(error)") - } + let bundle = testBundle + + let jsonData = try bundle.loadJSON("JSONFile") + let jsonObject = try decoder.decode(JSONFileData.self, from: jsonData) + + // Test for contents of JSON file + let bool = jsonObject.boolValue + #expect(bool == true, "bool should be true") + + let dictionary = jsonObject.dictionaryValue + + let string = dictionary.stringValue + #expect(string == "foo", "string should be \"foo\"") + + let double = dictionary.doubleValue + #expect(double == 2.1, "double should be 2.1") + + let array = dictionary.arrayValue + #expect(array == [2, 3], "array should be [2, 3]") } - func testMissingJSON() { + @Test("Attempt to load missing JSON file") + func missingJSON() async throws { // This test file is not present in the bundle. - let bundle = Bundle(for: JSONFileTests.self) - do { - let jsonObject = try bundle.loadJSON("MissingJSONFile") - XCTAssertNil(jsonObject, "should not get here") + let bundle = testBundle + + #expect(throws: (any Error).self) { + _ = try bundle.loadJSON("MissingJSONFile") } - catch let error { - XCTAssertTrue(error is ServiceError, "expected ServiceError.runtimeError, got \(error)") + + // Verify it's specifically a ServiceError + do { + _ = try bundle.loadJSON("MissingJSONFile") + Issue.record("Should have thrown an error") + } catch { + #expect(error is ServiceError, "expected ServiceError, got \(error)") } } - func testInvalidJSON() { + @Test("Attempt to parse invalid JSON file") + func invalidJSON() async throws { // This test file contains errors in formatting. - let bundle = Bundle(for: JSONFileTests.self) - do { + let bundle = testBundle + + #expect(throws: (any Error).self) { let jsonData = try bundle.loadJSON("InvalidJSONFile") - let jsonObject = try decoder.decode(AnyFileData.self, from: jsonData) - XCTAssertNil(jsonObject, "should not get here") - } - catch let error { - print("Successfully caught \(error)") - // OK we got here. it's an error NSCocoaErrorDomain Code 3840 "No value for key in object around character 41." + _ = try decoder.decode(AnyFileData.self, from: jsonData) } } - func testHalfBakedJSON() { + @Test("Attempt to parse half-baked JSON file") + func halfBakedJSON() async throws { // This test file lacks a dictionary at the root. - let bundle = Bundle(for: JSONFileTests.self) - do { + let bundle = testBundle + + #expect(throws: (any Error).self) { let jsonData = try bundle.loadJSON("HalfBakedJSONFile") - let jsonObject = try decoder.decode(AnyFileData.self, from: jsonData) - XCTAssertNil(jsonObject, "should not get here") - } - catch let error { - print("Successfully caught \(error)") - // OK we got here. it's an error NSCocoaErrorDomain Code 3840 "JSON text did not start with an array or object and option to allow fragments not set." + _ = try decoder.decode(AnyFileData.self, from: jsonData) } } } diff --git a/RolePlayingCore/RolePlayingCoreTests/NameGeneratorTests.swift b/RolePlayingCore/RolePlayingCoreTests/NameGeneratorTests.swift index 95dbda8..1c9d99b 100644 --- a/RolePlayingCore/RolePlayingCoreTests/NameGeneratorTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/NameGeneratorTests.swift @@ -6,19 +6,21 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest +import Testing import RolePlayingCore /// Use a mock random number generator so we can hardcode expected generated names. -class NameGeneratorTests: XCTestCase { +@Suite("Name Generator Tests") +struct NameGeneratorTests { - func testNameGenerator() { - let bundle = Bundle(for: NameGeneratorTests.self) - let data = try! bundle.loadJSON("TestNames") + @Test("Name generator produces expected names with mock generator") + func nameGenerator() throws { + let bundle = testBundle + let data = try bundle.loadJSON("TestNames") let decoder = JSONDecoder() - let nameGenerator = try! decoder.decode(NameGenerator.self, from: data) + let nameGenerator = try decoder.decode(NameGenerator.self, from: data) var generator = MockIndexGenerator() let expectedNames = ["Abadh", "Eunach", "Aillach", "Alsearbore", "Aod", "Aodel", "Edan", "Aodvoda", "Argcran", "Art"] @@ -26,7 +28,7 @@ class NameGeneratorTests: XCTestCase { for index in 0..<10 { let name = nameGenerator.makeName(using: &generator) generatedNames.append(name) - XCTAssertEqual(expectedNames[index], name, "expected generated name") + #expect(expectedNames[index] == name, "Expected generated name to match") } print("Generated names: \(generatedNames)") @@ -34,7 +36,8 @@ class NameGeneratorTests: XCTestCase { let _ = nameGenerator.makeName() } - func testRandomNumberGenerator() { + @Test("Random number generator produces sequential indices") + func randomNumberGenerator() { var generator = MockIndexGenerator() let testNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] diff --git a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift index fbe335c..62a85aa 100644 --- a/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/PlayerTests.swift @@ -6,24 +6,24 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class PlayerTests: XCTestCase { +@Suite("Player Tests") +struct PlayerTests { - var soldierTraits: Data! - var soldier: BackgroundTraits! - var humanTraits: Data! - var human: SpeciesTraits! - var fighterTraits: Data! - var fighter: ClassTraits! + let decoder = JSONDecoder() + let soldierTraits: Data + let soldier: BackgroundTraits + let humanTraits: Data + let human: SpeciesTraits + let fighterTraits: Data + let fighter: ClassTraits - override func setUp() { + init() throws { // TODO: Need to initialize UnitCurrency before creating Money instances in Player class. // Only load once. TODO: this has a side effect on other unit tests: currencies are already loaded. - let bundle = Bundle(for: PlayerTests.self) - let decoder = JSONDecoder() + let bundle = testBundle let data = try! bundle.loadJSON("TestCurrencies") _ = try! decoder.decode(Currencies.self, from: data) @@ -36,7 +36,7 @@ class PlayerTests: XCTestCase { "tool proficiency": "Gaming Set", "equipment": [["Spear", "Shortbow", "20 Arrows", "Gaming Set", "Healer's Kit", "Quiver", "Traveler's Clothes", "14 GP"], ["50 GP"]] } - """.data(using: .utf8) + """.data(using: .utf8)! self.soldier = try! decoder.decode(BackgroundTraits.self, from: self.soldierTraits) self.fighterTraits = """ @@ -50,7 +50,7 @@ class PlayerTests: XCTestCase { "starting wealth": "5d4x10", "experience points": [0, 300, 900, 2700] } - """.data(using: .utf8) + """.data(using: .utf8)! self.fighter = try! decoder.decode(ClassTraits.self, from: self.fighterTraits) self.humanTraits = """ @@ -66,44 +66,44 @@ class PlayerTests: XCTestCase { "languages": ["Common"], "extra languages": 1 } - """.data(using: .utf8) + """.data(using: .utf8)! self.human = try! decoder.decode(SpeciesTraits.self, from: self.humanTraits) } - func testPlayer() { + @Test("Create player with basic traits") + func player() async throws { let player = Player("Frodo", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .female, alignment: Alignment(.lawful, .neutral)) - XCTAssertEqual(player.name, "Frodo", "player name") - XCTAssertEqual(player.className, "Fighter", "class name") - XCTAssertEqual(player.speciesName, "Human", "species name") + #expect(player.name == "Frodo", "player name") + #expect(player.className == "Fighter", "class name") + #expect(player.speciesName == "Human", "species name") - XCTAssertEqual(player.descriptiveTraits.count, 0, "descriptiveTraits") + #expect(player.descriptiveTraits.count == 0, "descriptiveTraits") - XCTAssertEqual(player.gender, Player.Gender.female, "gender") - XCTAssertEqual(player.alignment, Alignment(.lawful, .neutral), "alignment") + #expect(player.gender == Player.Gender.female, "gender") + #expect(player.alignment == Alignment(.lawful, .neutral), "alignment") // Abilities is scores plus species modifiers, so + 1 for key in player.abilities.abilities { let score = player.abilities[key]! - XCTAssertTrue((4...19).contains(score), "ability score \(score) for \(key)") + #expect((3...20).contains(score), "ability score \(score) for \(key)") } // I do the maths - XCTAssertTrue((4..<7).contains(player.height.value), "height \(player.height.value)") + #expect((4..<7).contains(player.height.value), "height \(player.height.value)") - XCTAssertTrue((1...10).contains(player.maximumHitPoints), "maximum hit points") - XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") - XCTAssertEqual("\(player.classTraits.hitDice)", "d10", "hit dice") - XCTAssertEqual(player.experiencePoints, 0, "experience points") - XCTAssertEqual(player.level, 1, "level") + #expect((1...10).contains(player.maximumHitPoints), "maximum hit points") + #expect(player.maximumHitPoints == player.currentHitPoints, "current hit points") + #expect("\(player.classTraits.hitDice)" == "d10", "hit dice") + #expect(player.experiencePoints == 0, "experience points") + #expect(player.level == 1, "level") - XCTAssertTrue((50...200).contains(player.money.value), "money \(player.money.value)") + #expect((50...200).contains(player.money.value), "money \(player.money.value)") - XCTAssertEqual(player.proficiencyBonus, 2, "proficiency bonus") + #expect(player.proficiencyBonus == 2, "proficiency bonus") } - func testMinimumTraitsPlayer() { - let decoder = JSONDecoder() - + @Test("Decode player with minimum required traits") + func minimumTraitsPlayer() async throws { let playerTraits = """ { "name": "Bilbo", @@ -120,40 +120,33 @@ class PlayerTests: XCTestCase { } """.data(using: .utf8)! - do { - let player = try decoder.decode(Player.self, from: playerTraits) - player.speciesTraits = human - player.classTraits = fighter - - XCTAssertEqual(player.name, "Bilbo", "player name") - XCTAssertEqual(player.className, "Fighter", "class name") - XCTAssertEqual(player.speciesName, "Human", "species name") - - XCTAssertEqual(player.gender, Player.Gender.male, "gender") - XCTAssertNil(player.alignment, "alignment") - - XCTAssertEqual(player.abilities[.dexterity], 14, "dexterity") - XCTAssertEqual(player.abilities[.charisma], 12, "charisma") - - XCTAssertEqual(player.height.value, 3.75, "height") - - XCTAssertEqual(player.maximumHitPoints, 10, "maximum hit points") - XCTAssertEqual(player.maximumHitPoints, player.currentHitPoints, "current hit points") - - XCTAssertEqual(player.experiencePoints, 0, "experience points") - XCTAssertEqual(player.level, 1, "level") - - XCTAssertEqual(player.money.value, 130, "money") - - } - catch let error { - XCTFail("decode player failed, error: \(error)") - } + let player = try decoder.decode(Player.self, from: playerTraits) + player.speciesTraits = human + player.classTraits = fighter + + #expect(player.name == "Bilbo", "player name") + #expect(player.className == "Fighter", "class name") + #expect(player.speciesName == "Human", "species name") + + #expect(player.gender == Player.Gender.male, "gender") + #expect(player.alignment == nil, "alignment") + + #expect(player.abilities[.dexterity] == 14, "dexterity") + #expect(player.abilities[.charisma] == 12, "charisma") + + #expect(player.height.value == 3.75, "height") + + #expect(player.maximumHitPoints == 10, "maximum hit points") + #expect(player.maximumHitPoints == player.currentHitPoints, "current hit points") + + #expect(player.experiencePoints == 0, "experience points") + #expect(player.level == 1, "level") + + #expect(player.money.value == 130, "money") } - func testOptionalPlayerTraits() { - let decoder = JSONDecoder() - + @Test("Decode player with optional traits and level up") + func optionalPlayerTraits() async throws { let playerTraits = """ { "name": "Bilbo", @@ -172,32 +165,28 @@ class PlayerTests: XCTestCase { } """.data(using: .utf8)! - do { - let player = try decoder.decode(Player.self, from: playerTraits) - player.speciesTraits = human - player.classTraits = fighter - - XCTAssertNil(player.gender, "gender") - XCTAssertEqual(player.alignment, Alignment(.lawful, .evil), "alignment") - - XCTAssertEqual(player.canLevelUp, true, "level up") - XCTAssertEqual("\(player.hitDice)", "2d10", "hit dice") - player.levelUp() - XCTAssertEqual(player.level, 3, "level") - XCTAssertTrue(player.maximumHitPoints > 15, "experience points") - - XCTAssertEqual(player.canLevelUp, false, "level up") - XCTAssertEqual("\(player.hitDice)", "3d10", "hit dice") - - player.levelUp() - XCTAssertEqual(player.level, 3, "level") - } - catch let error { - XCTFail("decode player failed, error: \(error)") - } + let player = try decoder.decode(Player.self, from: playerTraits) + player.speciesTraits = human + player.classTraits = fighter + + #expect(player.gender == nil, "gender") + #expect(player.alignment == Alignment(.lawful, .evil), "alignment") + + #expect(player.canLevelUp == true, "level up") + #expect("\(player.hitDice)" == "2d10", "hit dice") + player.levelUp() + #expect(player.level == 3, "level") + #expect(player.maximumHitPoints > 15, "experience points") + + #expect(player.canLevelUp == false, "level up") + #expect("\(player.hitDice)" == "3d10", "hit dice") + + player.levelUp() + #expect(player.level == 3, "level") } - func testPlayerRoundTrip() { + @Test("Encode and decode player round trip") + func playerRoundTrip() async throws { let playerTraits = """ { "name": "Bilbo", @@ -217,110 +206,67 @@ class PlayerTests: XCTestCase { } """.data(using: .utf8)! - let decoder = JSONDecoder() - let player = try? decoder.decode(Player.self, from: playerTraits) + let player = try #require(try? decoder.decode(Player.self, from: playerTraits)) let encoder = JSONEncoder() - let encodedPlayer = try! encoder.encode(player) - let encoded = try? JSONSerialization.jsonObject(with: encodedPlayer, options: []) - XCTAssertNotNil(encoded, "player traits round trip") - - if let encoded = encoded as? [String: Any] { - XCTAssertEqual(encoded["name"] as? String, "Bilbo", "player traits round trip name") - XCTAssertEqual(encoded["gender"] as? String, "Male", "player traits round trip gender") - XCTAssertNotNil(encoded["alignment"] as? [String: Double], "player traits round trip alignment") - if let alignment = encoded["alignment"] as? [String: Double] { - XCTAssertEqual(alignment["ethics"], 0, "player traits round trip alignment ethics") - XCTAssertEqual(alignment["morals"], 1, "player traits round trip alignment ethics") - } - XCTAssertEqual(encoded["height"] as? String, "3.75 ft", "player traits round trip height") - - let abilities = encoded["ability scores"] as? [String: Int] - XCTAssertNotNil(abilities) - print("\(String(describing: abilities))") - XCTAssertEqual(abilities?["Dexterity"], 13, "player traits round trip ability scores") - - let backgroundAbilities = encoded["background ability scores"] as? [String] - XCTAssertNotNil(backgroundAbilities) - XCTAssertEqual(backgroundAbilities?.count, 3, "player traits round trip background ability scores count") - XCTAssertTrue(backgroundAbilities!.contains("Strength"), "player traits round trip background ability scores") - XCTAssertFalse(backgroundAbilities!.contains("Charisma"), "player traits round trip background ability scores") - - XCTAssertEqual(encoded["money"] as? String, "130.0 gp", "player traits round trip money") - XCTAssertEqual(encoded["maximum hit points"] as? Int, 20, "player traits round trip maximum hit points") - XCTAssertEqual(encoded["current hit points"] as? Int, 9, "player traits round trip current hit points") - XCTAssertEqual(encoded["level"] as? Int, 2, "player traits round trip level") - } else { - XCTFail("Failed to deserialize encoded Player into dictionary") - } - } - - func testMissingTraits() { - let decoder = JSONDecoder() - - // Test that each missing trait results in nil - do { - let traits = "{:}".data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } + let encodedPlayer = try encoder.encode(player) + let encoded = try #require(try? JSONSerialization.jsonObject(with: encodedPlayer, options: []) as? [String: Any]) - do { - let traits = """ - { - "name": "Bilbo" - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } + #expect(encoded["name"] as? String == "Bilbo", "player traits round trip name") + #expect(encoded["gender"] as? String == "Male", "player traits round trip gender") + let alignment = try #require(encoded["alignment"] as? [String: Double]) + #expect(alignment["ethics"] == 0, "player traits round trip alignment ethics") + #expect(alignment["morals"] == 1, "player traits round trip alignment morals") - do { - let traits = """ - { - "name": "Bilbo", - "height": "3'9\\"" - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } + #expect(encoded["height"] as? String == "3.75 ft", "player traits round trip height") - do { - let traits = """ - { - "name": "Bilbo", - "height": "3'9\\"", - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } + let abilities = try #require(encoded["ability scores"] as? [String: Int]) + #expect(abilities["Dexterity"] == 13, "player traits round trip ability scores") - do { - let traits = """ - { - "name": "Bilbo", - "height": "3'9\\"", - "ability scores": {"Dexterity": 13} - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) - } + let backgroundAbilities = try #require(encoded["background ability scores"] as? [String]) + #expect(backgroundAbilities.count == 3, "player traits round trip background ability scores count") + #expect(backgroundAbilities.contains("Strength"), "player traits round trip background ability scores") + #expect(!backgroundAbilities.contains("Charisma"), "player traits round trip background ability scores") - do { - let traits = """ - { - "name": "Bilbo", - "height": "3'9\\"", - "ability scores": {"Dexterity": 13}, - "money": 130] - } - """.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) - XCTAssertNil(player) + #expect(encoded["money"] as? String == "130.0 gp", "player traits round trip money") + #expect(encoded["maximum hit points"] as? Int == 20, "player traits round trip maximum hit points") + #expect(encoded["current hit points"] as? Int == 9, "player traits round trip current hit points") + #expect(encoded["level"] as? Int == 2, "player traits round trip level") + } + + @Test("Verify missing required traits cause decode failure", arguments: [ + "{:}", + """ + { + "name": "Bilbo" + } + """, + """ + { + "name": "Bilbo", + "height": "3'9\\"", + } + """, + """ + { + "name": "Bilbo", + "height": "3'9\\"", + "ability scores": {"Dexterity": 13} + } + """, + """ + { + "name": "Bilbo", + "height": "3'9\\"", + "ability scores": {"Dexterity": 13}, + "money": 130] } + """ + ]) + func missingTraits(json: String) async throws { + let traits = json.data(using: .utf8)! + let player = try? decoder.decode(Player.self, from: traits) + #expect(player == nil) } func expectedModifier(for abilityScore: Int) -> Int { @@ -328,84 +274,56 @@ class PlayerTests: XCTestCase { return selfMinus10 < 0 ? Int((Double(selfMinus10) / 2.0).rounded(.down)) : selfMinus10 / 2 } - func testComputedProperties() { + @Test("Verify computed properties") + func computedProperties() async throws { let player = Player("Gandalf", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.neutral, .good)) // Test speed (from species traits) - XCTAssertEqual(player.speed, 30, "speed should match species speed") - XCTAssertEqual(player.size, .medium, "size should match species size") + #expect(player.speed == 30, "speed should match species speed") + #expect(player.size == .medium, "size should match species size") // Test modifiers for ability in player.modifiers.abilities { let abilityScore = player.abilities[ability]! let expectedModifier = expectedModifier(for: abilityScore) - XCTAssertEqual(player.modifiers[ability], expectedModifier, "modifier calculation") + #expect(player.modifiers[ability] == expectedModifier, "modifier calculation") } // Test initiative - XCTAssertEqual(player.initiativeModifier, player.modifiers[.dexterity], "initiative modifier") - XCTAssertEqual(player.initiativeScore, 10 + player.modifiers[.dexterity], "initiative score") + #expect(player.initiativeModifier == player.modifiers[.dexterity], "initiative modifier") + #expect(player.initiativeScore == 10 + player.modifiers[.dexterity], "initiative score") // Test passive perception - XCTAssertEqual(player.passivePerception, 10 + player.modifiers[.wisdom], "passive perception") + #expect(player.passivePerception == 10 + player.modifiers[.wisdom], "passive perception") } - func testProficiencyBonus() { + @Test("Verify proficiency bonus at different levels", arguments: [ + (1, 2), (4, 2), + (5, 3), (8, 3), + (9, 4), (12, 4), + (13, 5), (16, 5), + (17, 6), (20, 6) + ]) + func proficiencyBonus(level: Int, expectedBonus: Int) async throws { let player = Player("Aragorn", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) - - // Level 1-4: +2 - player.level = 1 - XCTAssertEqual(player.proficiencyBonus, 2, "proficiency bonus at level 1") - - player.level = 4 - XCTAssertEqual(player.proficiencyBonus, 2, "proficiency bonus at level 4") - - // Level 5-8: +3 - player.level = 5 - XCTAssertEqual(player.proficiencyBonus, 3, "proficiency bonus at level 5") - - player.level = 8 - XCTAssertEqual(player.proficiencyBonus, 3, "proficiency bonus at level 8") - - // Level 9-12: +4 - player.level = 9 - XCTAssertEqual(player.proficiencyBonus, 4, "proficiency bonus at level 9") - - player.level = 12 - XCTAssertEqual(player.proficiencyBonus, 4, "proficiency bonus at level 12") - - // Level 13-16: +5 - player.level = 13 - XCTAssertEqual(player.proficiencyBonus, 5, "proficiency bonus at level 13") - - player.level = 16 - XCTAssertEqual(player.proficiencyBonus, 5, "proficiency bonus at level 16") - - // Level 17-20: +6 - player.level = 17 - XCTAssertEqual(player.proficiencyBonus, 6, "proficiency bonus at level 17") - - player.level = 20 - XCTAssertEqual(player.proficiencyBonus, 6, "proficiency bonus at level 20") + player.level = level + #expect(player.proficiencyBonus == expectedBonus, "proficiency bonus at level \(level)") } - func testHitDiceAtDifferentLevels() { + @Test("Verify hit dice at different levels", arguments: [ + (1, "d10"), + (5, "5d10"), + (10, "10d10"), + (20, "20d10") + ]) + func hitDiceAtDifferentLevels(level: Int, expectedDice: String) async throws { let player = Player("Legolas", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) - - player.level = 1 - XCTAssertEqual("\(player.hitDice)", "d10", "hit dice at level 1") - - player.level = 5 - XCTAssertEqual("\(player.hitDice)", "5d10", "hit dice at level 5") - - player.level = 10 - XCTAssertEqual("\(player.hitDice)", "10d10", "hit dice at level 10") - - player.level = 20 - XCTAssertEqual("\(player.hitDice)", "20d10", "hit dice at level 20") + player.level = level + #expect("\(player.hitDice)" == expectedDice, "hit dice at level \(level)") } - func testHashableConformance() { + @Test("Verify Hashable conformance") + func hashableConformance() async throws { let player1 = Player("Gimli", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male, alignment: Alignment(.lawful, .good)) player1.descriptiveTraits = ["ideal": "Honor", "bond": "My axe"] @@ -421,7 +339,7 @@ class PlayerTests: XCTestCase { player2.descriptiveTraits = ["ideal": "Honor", "bond": "My axe"] // Test equality - XCTAssertEqual(player1, player2, "identical players should be equal") + #expect(player1 == player2, "identical players should be equal") // Test hash values var hasher1 = Hasher() @@ -432,19 +350,20 @@ class PlayerTests: XCTestCase { player2.hash(into: &hasher2) let hash2 = hasher2.finalize() - XCTAssertEqual(hash1, hash2, "identical players should have same hash") + #expect(hash1 == hash2, "identical players should have same hash") // Test that players can be used in Sets let playerSet: Set = [player1, player2] - XCTAssertEqual(playerSet.count, 1, "set should contain only one unique player") + #expect(playerSet.count == 1, "set should contain only one unique player") } - func testPlayerInequality() { + @Test("Verify player inequality") + func playerInequality() async throws { let player1 = Player("Boromir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) let player2 = Player("Faramir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) // Different names - XCTAssertNotEqual(player1, player2, "players with different names should not be equal") + #expect(player1 != player2, "players with different names should not be equal") // Different hit points let player3 = Player("Boromir", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) @@ -453,26 +372,28 @@ class PlayerTests: XCTestCase { player3.money = player1.money player3.currentHitPoints = player3.currentHitPoints - 5 - XCTAssertNotEqual(player1, player3, "players with different current HP should not be equal") + #expect(player1 != player3, "players with different current HP should not be equal") } - func testGenderCases() { + @Test("Verify gender cases") + func genderCases() async throws { // Test all gender cases let female = Player("Diana", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .female) - XCTAssertEqual(female.gender, .female, "female gender") + #expect(female.gender == .female, "female gender") let male = Player("Arthur", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: .male) - XCTAssertEqual(male.gender, .male, "male gender") + #expect(male.gender == .male, "male gender") let agender = Player("Riley", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter, gender: nil) - XCTAssertNil(agender.gender, "nil gender for androgynous/hermaphroditic") + #expect(agender.gender == nil, "nil gender for androgynous/hermaphroditic") } - func testDescriptiveTraits() { + @Test("Verify descriptive traits") + func descriptiveTraits() async throws { let player = Player("Samwise", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) // Initially empty - XCTAssertEqual(player.descriptiveTraits.count, 0) + #expect(player.descriptiveTraits.count == 0) // Add traits player.descriptiveTraits["ideal"] = "Loyalty" @@ -480,61 +401,64 @@ class PlayerTests: XCTestCase { player.descriptiveTraits["flaw"] = "Too trusting" player.descriptiveTraits["background"] = "Gardener" - XCTAssertEqual(player.descriptiveTraits.count, 4) - XCTAssertEqual(player.descriptiveTraits["ideal"], "Loyalty") - XCTAssertEqual(player.descriptiveTraits["bond"], "My friends") - XCTAssertEqual(player.descriptiveTraits["flaw"], "Too trusting") - XCTAssertEqual(player.descriptiveTraits["background"], "Gardener") + #expect(player.descriptiveTraits.count == 4) + #expect(player.descriptiveTraits["ideal"] == "Loyalty") + #expect(player.descriptiveTraits["bond"] == "My friends") + #expect(player.descriptiveTraits["flaw"] == "Too trusting") + #expect(player.descriptiveTraits["background"] == "Gardener") } - func testAbilityScoresRoll() { + @Test("Verify ability scores roll") + func abilityScoresRoll() async throws { var abilities = AbilityScores() abilities.roll() // Verify all abilities have valid scores (4d6-L should give 3-18) for ability in abilities.abilities { let score = abilities[ability]! - XCTAssertTrue((3...18).contains(score), "rolled ability score should be between 3 and 18") + #expect((3...18).contains(score), "rolled ability score should be between 3 and 18") } // Verify all six abilities are set - XCTAssertEqual(abilities.abilities.count, 6, "should have 6 abilities") + #expect(abilities.abilities.count == 6, "should have 6 abilities") } - func testDiceHitDiceExtension() { + @Test("Verify dice hit dice extension") + func diceHitDiceExtension() async throws { // Test the hitDice extension on Dice let d6 = SimpleDice(.d6) let level1HitDice = d6.hitDice(level: 1) - XCTAssertEqual("\(level1HitDice)", "d6") + #expect("\(level1HitDice)" == "d6") let level5HitDice = d6.hitDice(level: 5) - XCTAssertEqual("\(level5HitDice)", "5d6") + #expect("\(level5HitDice)" == "5d6") let level10HitDice = d6.hitDice(level: 10) - XCTAssertEqual("\(level10HitDice)", "10d6") + #expect("\(level10HitDice)" == "10d6") } - func testSpeciesAndClassTraitsDidSet() { + @Test("Verify species and class traits didSet") + func speciesAndClassTraitsDidSet() async throws { let player = Player("Test", backgroundTraits: soldier, speciesTraits: human, classTraits: fighter) - XCTAssertEqual(player.backgroundName, "Soldier") - XCTAssertEqual(player.speciesName, "Human") - XCTAssertEqual(player.className, "Fighter") + #expect(player.backgroundName == "Soldier") + #expect(player.speciesName == "Human") + #expect(player.className == "Fighter") // Create a mock second species (we'll reuse human but check the didSet is called) - let mockSpecies = human! + let mockSpecies = human player.speciesTraits = mockSpecies - XCTAssertEqual(player.speciesName, mockSpecies.name) + #expect(player.speciesName == mockSpecies.name) // Create a mock second class (we'll reuse fighter but check the didSet is called) - let mockClass = fighter! + let mockClass = fighter player.classTraits = mockClass - XCTAssertEqual(player.className, mockClass.name) + #expect(player.className == mockClass.name) } - func testPlayerEncodingWithDescriptiveTraits() { - let decoder = JSONDecoder() + @Test("Encode and decode player with descriptive traits") + func playerEncodingWithDescriptiveTraits() async throws { let encoder = JSONEncoder() let playerTraits = """ @@ -557,37 +481,32 @@ class PlayerTests: XCTestCase { } """.data(using: .utf8)! - do { - let player = try decoder.decode(Player.self, from: playerTraits) - XCTAssertEqual(player.descriptiveTraits.count, 3) - XCTAssertEqual(player.descriptiveTraits["ideal"], "Adventure") - XCTAssertEqual(player.descriptiveTraits["bond"], "The Shire") - XCTAssertEqual(player.descriptiveTraits["flaw"], "Impulsive") - - // Test encoding - let encoded = try encoder.encode(player) - let decodedDict = try JSONSerialization.jsonObject(with: encoded) as? [String: Any] - let encodedTraits = decodedDict?["descriptive traits"] as? [String: String] - XCTAssertNotNil(encodedTraits) - XCTAssertEqual(encodedTraits?["ideal"], "Adventure") - } catch { - XCTFail("Failed to decode/encode player with descriptive traits: \(error)") - } + let player = try decoder.decode(Player.self, from: playerTraits) + #expect(player.descriptiveTraits.count == 3) + #expect(player.descriptiveTraits["ideal"] == "Adventure") + #expect(player.descriptiveTraits["bond"] == "The Shire") + #expect(player.descriptiveTraits["flaw"] == "Impulsive") + + // Test encoding + let encoded = try encoder.encode(player) + let decodedDict = try #require(try? JSONSerialization.jsonObject(with: encoded) as? [String: Any]) + let encodedTraits = try #require(decodedDict["descriptive traits"] as? [String: String]) + #expect(encodedTraits["ideal"] == "Adventure") } - func testRollHitPointsClassMethod() { + @Test("Verify rollHitPoints class method") + func rollHitPointsClassMethod() async throws { // Test the static rollHitPoints method let hitPoints = Player.rollHitPoints(classTraits: fighter, speciesTraits: human) // Fighter has d10, so minimum should be 6 (max(10/2+1, roll)), // but roll could be lower, so minimum is actually 6 + species bonus // Maximum is 10 + species bonus - XCTAssertTrue((6...10).contains(hitPoints), "hit points should be in valid range") + #expect((6...10).contains(hitPoints), "hit points should be in valid range") } - func testMultipleLevelUps() { - let decoder = JSONDecoder() - + @Test("Verify multiple level ups") + func multipleLevelUps() async throws { let playerTraits = """ { "name": "Merry", @@ -605,7 +524,7 @@ class PlayerTests: XCTestCase { } """.data(using: .utf8)! - let player = try! decoder.decode(Player.self, from: playerTraits) + let player = try decoder.decode(Player.self, from: playerTraits) player.speciesTraits = human player.classTraits = fighter @@ -613,27 +532,27 @@ class PlayerTests: XCTestCase { // Add enough XP to level up to level 2 player.experiencePoints = 301 - XCTAssertTrue(player.canLevelUp) + #expect(player.canLevelUp) player.levelUp() - XCTAssertEqual(player.level, 2) - XCTAssertTrue(player.maximumHitPoints > initialHP, "HP should increase on level up") + #expect(player.level == 2) + #expect(player.maximumHitPoints > initialHP, "HP should increase on level up") // Add enough XP to level up to level 3 player.experiencePoints = 901 - XCTAssertTrue(player.canLevelUp) + #expect(player.canLevelUp) player.levelUp() - XCTAssertEqual(player.level, 3) + #expect(player.level == 3) // Add enough XP to level up to level 4 player.experiencePoints = 2701 - XCTAssertTrue(player.canLevelUp) + #expect(player.canLevelUp) player.levelUp() - XCTAssertEqual(player.level, 4) + #expect(player.level == 4) // Without enough XP, cannot level up player.experiencePoints = 2701 - XCTAssertFalse(player.canLevelUp) + #expect(player.canLevelUp == false) player.levelUp() // Should do nothing - XCTAssertEqual(player.level, 4) + #expect(player.level == 4) } } diff --git a/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift b/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift index e74f96b..a8b8a66 100644 --- a/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/PlayersTests.swift @@ -6,99 +6,71 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class PlayersTests: XCTestCase { +@Suite("Players Tests") +struct PlayersTests { - let bundle = Bundle(for: PlayersTests.self) + let bundle = testBundle let decoder = JSONDecoder() + let backgrounds: Backgrounds + let classes: Classes + let species: Species - var backgrounds: Backgrounds! - var classes: Classes! - var species: Species! - - override func setUp() { + init() throws { // TODO: Need to initialize UnitCurrency before creating Money instances in Player class. let currenciesData = try! bundle.loadJSON("TestCurrencies") _ = try! decoder.decode(Currencies.self, from: currenciesData) let backgroundsData = try! bundle.loadJSON("TestBackgrounds") - backgrounds = try! decoder.decode(Backgrounds.self, from: backgroundsData) + self.backgrounds = try! decoder.decode(Backgrounds.self, from: backgroundsData) let classesData = try! bundle.loadJSON("TestClasses") - classes = try! decoder.decode(Classes.self, from: classesData) + self.classes = try! decoder.decode(Classes.self, from: classesData) let speciesData = try! bundle.loadJSON("TestSpecies") - species = try! decoder.decode(Species.self, from: speciesData) + self.species = try! decoder.decode(Species.self, from: speciesData) } - func testPlayers() { + @Test("Load and manipulate players collection") + func players() async throws { + let playersData = try bundle.loadJSON("TestPlayers") + let players = try decoder.decode(Players.self, from: playersData) + try players.resolve(backgrounds: backgrounds, classes: classes, species: species) - var players: Players! = nil - do { - let playersData = try bundle.loadJSON("TestPlayers") - players = try decoder.decode(Players.self, from: playersData) - try players.resolve(backgrounds: backgrounds, classes: classes, species: species) - } - catch let error { - XCTFail("players.load failed, error \(error)") - } - XCTAssertEqual(players.players.count, 2, "players count") - XCTAssertEqual(players.count, 2, "players count") + #expect(players.players.count == 2, "players count") + #expect(players.count == 2, "players count") - let removedPlayer = players[0]! + let removedPlayer = try #require(players[0]) players.remove(at: 0) - XCTAssertEqual(players.count, 1, "players count") + #expect(players.count == 1, "players count") players.insert(removedPlayer, at: 1) - XCTAssertEqual(players.count, 2, "players count") - XCTAssertTrue(players[1]! === removedPlayer, "players count") + #expect(players.count == 2, "players count") + #expect(players[1]! === removedPlayer, "players count") } - func testMissingTraits() { - do { - let playersData = try! bundle.loadJSON("InvalidClassPlayers") - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(backgrounds: backgrounds, classes: classes, species: species) - XCTFail("players.load should have failed") - } - catch let error { - print("players.load correctly threw an error \(error)") - } - - do { - let playersData = try! bundle.loadJSON("InvalidSpeciesPlayers") - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(backgrounds: backgrounds, classes: classes, species: species) - XCTFail("players.resolve should have failed") - } - catch let error { - print("players.resolve correctly threw an error \(error)") - } + @Test("Verify missing or invalid traits cause resolution failure", arguments: [ + "InvalidClassPlayers", + "InvalidSpeciesPlayers", + "MissingClassPlayers", + "MissingSpeciesPlayers" + ]) + func missingTraits(jsonFile: String) async throws { + let playersData = try bundle.loadJSON(jsonFile) + // Attempt to decode and resolve, expecting an error to be thrown + // Error could occur during decoding (missing required fields) or resolution (invalid references) do { - let playersData = try! bundle.loadJSON("MissingClassPlayers") - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(backgrounds: backgrounds, classes: classes, species: species) - XCTFail("players.resolve should have failed") - } - catch let error { - print("players.resolve correctly threw an error \(error)") - } - - do { - let playersData = try! bundle.loadJSON("MissingSpeciesPlayers") let players = try decoder.decode(Players.self, from: playersData) try players.resolve(backgrounds: backgrounds, classes: classes, species: species) - XCTFail("players.resolve should have failed") - } - catch let error { - print("players.resolve correctly threw an error \(error)") + // If we reach here, no error was thrown - the test should fail + Issue.record("Expected an error to be thrown for \(jsonFile), but none was thrown") + } catch { + // Success - an error was thrown as expected + // Optionally, you could verify the specific error type or message here } } - - } diff --git a/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift b/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift index 3341912..c0b3bc5 100644 --- a/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/ServiceErrorTests.swift @@ -6,23 +6,23 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class ServiceErrorTests: XCTestCase { +@Suite("Service Error Tests") +struct ServiceErrorTests { - func testServiceError() { + @Test("Verify ServiceError contains expected information") + func serviceError() async throws { do { throw RuntimeError("Gah!") - } - catch let error { - XCTAssertTrue(error is ServiceError, "should be a service error") + } catch { + #expect(error is ServiceError, "should be a service error") let description = "\(error)" - XCTAssertTrue(description.contains("Runtime error"), "should be a runtime error") - XCTAssertTrue(description.contains("Gah!"), "should contain the message") - XCTAssertTrue(description.contains("testServiceError"), "should have throw function name in it") - XCTAssertTrue(description.contains("ServiceErrorTests"), "should have throw file name in it") + #expect(description.contains("Runtime error"), "should be a runtime error") + #expect(description.contains("Gah!"), "should contain the message") + #expect(description.contains("serviceError"), "should have throw function name in it") + #expect(description.contains("ServiceErrorTests"), "should have throw file name in it") } } } diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesNamesTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesNamesTests.swift index 195a265..d8ebb11 100644 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesNamesTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/SpeciesNamesTests.swift @@ -6,50 +6,52 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest +import Testing @testable import RolePlayingCore -class SpeciesNamesTests: XCTestCase { +@Suite("Species Names") +struct SpeciesNamesTests { - func testSpeciesNames() { - let bundle = Bundle(for: SpeciesNamesTests.self) - do { - let data = try bundle.loadJSON("TestSpeciesNames") - let decoder = JSONDecoder() - let speciesNames = try decoder.decode(SpeciesNames.self, from: data) - - XCTAssertEqual(speciesNames.names.count, 10, "Number of species name families") - - // TODO: find a way to test just the minimum functionality. - // In the meantime, use the test species. - let bundle = Bundle(for: SpeciesNamesTests.self) - let jsonData = try bundle.loadJSON("TestSpecies") - let species = try decoder.decode(Species.self, from: jsonData) - let moreJsonData = try bundle.loadJSON("TestMoreSpecies") - let moreSpecies = try decoder.decode(Species.self, from: moreJsonData) - - let allSpecies = Species() - allSpecies.species = species.species + moreSpecies.species - - // TODO: random names are hard; for now, get code coverage. - do { - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Human")!, gender: .female) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Elf")!, gender: .male) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Mountain Dwarf")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Stout")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Half-Elf")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Half-Orc")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Dragonborn")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allSpecies.find("Tiefling")!, gender: nil) - - } - - let encoder = JSONEncoder() - _ = try encoder.encode(speciesNames) - } - catch let error { - XCTFail("error thrown: \(error)") - } + @Test("Loading and generating species names") + func speciesNames() async throws { + let bundle = testBundle + let data = try bundle.loadJSON("TestSpeciesNames") + let decoder = JSONDecoder() + let speciesNames = try decoder.decode(SpeciesNames.self, from: data) + + #expect(speciesNames.names.count == 8, "Number of species name families") + + // TODO: find a way to test just the minimum functionality. + // In the meantime, use the test species. + let jsonData = try bundle.loadJSON("TestSpecies") + let species = try decoder.decode(Species.self, from: jsonData) + let moreJsonData = try bundle.loadJSON("TestMoreSpecies") + let moreSpecies = try decoder.decode(Species.self, from: moreJsonData) + + let allSpecies = Species() + allSpecies.species = species.species + moreSpecies.species + + // TODO: random names are hard; for now, get code coverage. + let human = try #require(allSpecies.find("Human")) + _ = speciesNames.randomName(speciesTraits: human, gender: .female) + + let elf = try #require(allSpecies.find("Elf")) + _ = speciesNames.randomName(speciesTraits: elf, gender: .male) + + let mountainDwarf = try #require(allSpecies.find("Mountain Dwarf")) + _ = speciesNames.randomName(speciesTraits: mountainDwarf, gender: nil) + + let stout = try #require(allSpecies.find("Stout")) + _ = speciesNames.randomName(speciesTraits: stout, gender: nil) + + let dragonborn = try #require(allSpecies.find("Dragonborn")) + _ = speciesNames.randomName(speciesTraits: dragonborn, gender: nil) + + let tiefling = try #require(allSpecies.find("Tiefling")) + _ = speciesNames.randomName(speciesTraits: tiefling, gender: nil) + + let encoder = JSONEncoder() + _ = try encoder.encode(speciesNames) } } diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift index 214dd65..9a78061 100644 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/SpeciesTests.swift @@ -6,55 +6,42 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class SpeciesTests: XCTestCase { +@Suite("Species Tests") +struct SpeciesTests { - let bundle = Bundle(for: SpeciesTests.self) + let bundle = testBundle let decoder = JSONDecoder() - func testDefaultInit() { + @Test("Default initialization creates empty species") + func defaultInit() async throws { let species = Species() - XCTAssertEqual(species.species.count, 0, "default init") + #expect(species.species.count == 0, "default init") } - func testSpecies() { - do { - let jsonData = try bundle.loadJSON("TestSpecies") - let species = try decoder.decode(Species.self, from: jsonData) - - XCTAssertNotNil(species, "Species file failed to load") - - XCTAssertEqual(species.leafSpecies.count, 8, "all species") - XCTAssertEqual(species.count, 11, "all species") - XCTAssertNotNil(species[0], "species by index") - - // Test finding a species by name - XCTAssertNotNil(species.find("Human"), "Fighter should be non-nil") - XCTAssertNil(species.find("Foo"), "Foo should be nil") - XCTAssertNil(species.find(nil), "nil species name should find nil") - } - catch let error { - XCTFail("Species threw an error: \(error)") - } + @Test("Load and parse species from JSON file") + func species() async throws { + let jsonData = try bundle.loadJSON("TestSpecies") + let species = try decoder.decode(Species.self, from: jsonData) + + #expect(species.leafSpecies.count == 8, "all species") + #expect(species.count == 11, "all species") + #expect(species[0] != nil, "species by index") + + // Test finding a species by name + #expect(species.find("Human") != nil, "Fighter should be non-nil") + #expect(species.find("Foo") == nil, "Foo should be nil") + #expect(species.find(nil) == nil, "nil species name should find nil") } - func testUncommonSpecies() { - // Test throwing constructor - do { - let jsonData = try bundle.loadJSON("TestMoreSpecies") - let species = try decoder.decode(Species.self, from: jsonData) - - XCTAssertNotNil(species, "Species file failed to load") - - // There should be 5 species plus 2 subspecies - XCTAssertEqual(species.species.count, 7, "all species") - - } - catch let error { - XCTFail("Species threw an error: \(error)") - } + @Test("Load uncommon species from JSON file") + func uncommonSpecies() async throws { + let jsonData = try bundle.loadJSON("TestMoreSpecies") + let species = try decoder.decode(Species.self, from: jsonData) + + // There should be 5 species plus 2 subspecies + #expect(species.species.count == 5, "all species") } } diff --git a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift index b7aa70f..956952c 100644 --- a/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/SpeciesTraitsTests.swift @@ -6,15 +6,16 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class SpeciesTraitsTests: XCTestCase { +@Suite("Species Traits Tests") +struct SpeciesTraitsTests { let decoder = JSONDecoder() - func testSpeciesTraits() { + @Test("Decode basic species traits") + func speciesTraits() async throws { let traits = """ { "name": "Human", @@ -25,23 +26,18 @@ class SpeciesTraitsTests: XCTestCase { "extra languages": 1 } """.data(using: .utf8)! - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") - } - XCTAssertNotNil(speciesTraits) - XCTAssertEqual(speciesTraits?.name, "Human", "name") - XCTAssertEqual(speciesTraits?.plural, "Humans", "plural") - XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") - XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") - XCTAssertEqual(speciesTraits?.speed, 30, "speed") + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + + #expect(speciesTraits.name == "Human", "name") + #expect(speciesTraits.plural == "Humans", "plural") + #expect(speciesTraits.aliases.count == 0, "aliases") + #expect(speciesTraits.lifespan == 90, "lifespan") + #expect(speciesTraits.speed == 30, "speed") } - func testMinimumTraits() { + @Test("Decode minimum required traits") + func minimumTraits() async throws { let traits = """ { "name": "Giant Human", @@ -50,22 +46,18 @@ class SpeciesTraitsTests: XCTestCase { "speed": 30 } """.data(using: .utf8)! - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") - } - XCTAssertNotNil(speciesTraits) - XCTAssertEqual(speciesTraits?.name, "Giant Human", "name") - XCTAssertEqual(speciesTraits?.plural, "Giant Humans", "plural") - XCTAssertEqual(speciesTraits?.lifespan, 90, "lifespan") - XCTAssertEqual(speciesTraits?.speed, 30, "speed") - XCTAssertEqual(speciesTraits?.aliases.count, 0, "aliases") + + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + + #expect(speciesTraits.name == "Giant Human", "name") + #expect(speciesTraits.plural == "Giant Humans", "plural") + #expect(speciesTraits.lifespan == 90, "lifespan") + #expect(speciesTraits.speed == 30, "speed") + #expect(speciesTraits.aliases.count == 0, "aliases") } - func testOptionalTraits() { + @Test("Decode optional traits like aliases") + func optionalTraits() async throws { let traits = """ { "name": "Small Human", @@ -76,119 +68,96 @@ class SpeciesTraitsTests: XCTestCase { } """.data(using: .utf8)! - var speciesTraits: SpeciesTraits? = nil - do { - speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - } - catch let error { - XCTFail("Failed to decode species traits, error: \(error)") - } - XCTAssertNotNil(speciesTraits) - XCTAssertEqual(speciesTraits?.aliases.count, 1, "aliases count") + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + + #expect(speciesTraits.aliases.count == 1, "aliases count") } - func testMissingTraits() { - // Test that each missing trait results in nil - do { - let traits = "{}".data(using: .utf8)! - let speciesTraits = try? decoder.decode(SpeciesTraits.self, from: traits) - XCTAssertNil(speciesTraits) - } + @Test("Verify missing required traits cause decode failure", arguments: [ + "{}", + """ + { "name": "Giant Human" } + """, + """ + { + "plural": "Giant Humans" + } + """ + ]) + func missingTraits(json: String) async throws { + let traits = json.data(using: .utf8)! - do { - let traits = """ - { "name": "Giant Human" } - """.data(using: .utf8)! - let speciesTraits = try? decoder.decode(SpeciesTraits.self, from: traits) - XCTAssertNil(speciesTraits) + #expect(throws: (any Error).self) { + _ = try decoder.decode(SpeciesTraits.self, from: traits) } - - do { - let traits = """ + } + + @Test("Decode species with subspecies") + func decodingSpeciesTraits() async throws { + let traits = """ + { + "name": "Human", + "plural": "Humans", + "lifespan": 90, + "speed": 30, + "subspecies": [ { - "plural": "Giant Humans" + "name": "Subhuman", + "plural": "Subhumans", + "lifespan": 60, + "speed": 10 } - """.data(using: .utf8)! - let speciesTraits = try? decoder.decode(SpeciesTraits.self, from: traits) - XCTAssertNil(speciesTraits) + ] } + """.data(using: .utf8)! + + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + let subspeciesTraits = try #require(speciesTraits.subspecies.first) + + #expect(subspeciesTraits.name == "Subhuman", "name") + #expect(subspeciesTraits.plural == "Subhumans", "plural") + #expect(subspeciesTraits.lifespan == 60, "lifespan") + #expect(subspeciesTraits.speed == 10, "speed") + #expect(subspeciesTraits.aliases.count == 0, "aliases") } - func testDecodingSpeciesTraits() { - do { - let traits = """ - { - "name": "Human", - "plural": "Humans", - "lifespan": 90, - "speed": 30, - "subspecies": [ - { - "name": "Subhuman", - "plural": "Subhumans", - "lifespan": 60, - "speed": 10 - } - ] - } - """.data(using: .utf8)! - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - if let subspeciesTraits = speciesTraits.subspecies.first { - XCTAssertEqual(subspeciesTraits.name, "Subhuman", "name") - XCTAssertEqual(subspeciesTraits.plural, "Subhumans", "plural") - XCTAssertEqual(subspeciesTraits.lifespan, 60, "lifespan") - XCTAssertEqual(subspeciesTraits.speed, 10, "speed") - XCTAssertEqual(subspeciesTraits.aliases.count, 0, "aliases") - } else { - XCTFail("decode failed for traits with subspecies traits") - } - } - catch let error { - XCTFail("decode failed, error: \(error)") + @Test("Subspecies inherit parent traits") + func subspeciesOverrides() async throws { + let traits = """ + { + "name": "Human", + "plural": "Humans", + "lifespan": 90, + "speed": 30, + "subspecies": [ + { + "name": "Folk", + "plural": "Folks", + "aliases": ["Plainfolk"], + "darkvision": 20 + } + ] } + """.data(using: .utf8)! - // Test the other half overrides - do { - let traits = """ - { - "name": "Human", - "plural": "Humans", - "lifespan": 90, - "speed": 30, - "subspecies": [ - { - "name": "Folk", - "plural": "Folks", - "aliases": ["Plainfolk"], - "darkvision": 20 - } - ] - } - """.data(using: .utf8)! - - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) - if let subspeciesTraits = speciesTraits.subspecies.first { - XCTAssertNotNil(subspeciesTraits) - XCTAssertEqual(subspeciesTraits.name, "Folk", "name") - XCTAssertEqual(subspeciesTraits.plural, "Folks", "plural") - XCTAssertEqual(subspeciesTraits.lifespan, 90, "lifespan") - XCTAssertEqual(subspeciesTraits.speed, 30, "speed") - XCTAssertEqual(subspeciesTraits.aliases.count, 1, "aliases") - XCTAssertEqual(subspeciesTraits.baseSizes, speciesTraits.baseSizes, "size") - } else { - XCTFail("decode failed for traits with subspecies traits") - } - } - catch let error { - XCTFail("decode failed with error: \(error)") - } + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + let subspeciesTraits = try #require(speciesTraits.subspecies.first) + + #expect(subspeciesTraits.name == "Folk", "name") + #expect(subspeciesTraits.plural == "Folks", "plural") + #expect(subspeciesTraits.lifespan == 90, "lifespan") + #expect(subspeciesTraits.speed == 30, "speed") + #expect(subspeciesTraits.aliases.count == 1, "aliases") + #expect(subspeciesTraits.baseSizes == speciesTraits.baseSizes, "size") } - func testEncodingSubspeciesTraits() { + @Test("Encode subspecies traits with blending") + func encodingSubspeciesTraits() async throws { let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], descriptiveTraits: [:], lifespan: 90, darkVision: 0, speed: 45) let encoder = JSONEncoder() + // Test 1: Subspecies with blended traits do { var copyOfSpeciesTraits = speciesTraits var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", lifespan: 45, darkVision: 0, speed: 30) @@ -199,27 +168,23 @@ class SpeciesTraitsTests: XCTestCase { let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as! [String: Any] // Confirm species traits - XCTAssertEqual(dictionary["name"] as? String, "Human", "encoding name") - XCTAssertEqual(dictionary["plural"] as? String, "Humans", "encoding name") - XCTAssertEqual(dictionary["lifespan"] as? Int, 90, "encoding lifespan") - XCTAssertEqual(dictionary["darkvision"] as? Int, 0, "encoding name") - XCTAssertEqual(dictionary["speed"] as? Int, 45, "encoding name") + #expect(dictionary["name"] as? String == "Human", "encoding name") + #expect(dictionary["plural"] as? String == "Humans", "encoding plural") + #expect(dictionary["lifespan"] as? Int == 90, "encoding lifespan") + #expect(dictionary["darkvision"] as? Int == 0, "encoding darkvision") + #expect(dictionary["speed"] as? Int == 45, "encoding speed") // Confirm subspecies traits - if let subspecies = dictionary["subspecies"] as? [[String: Any]], let firstSubspecies = subspecies.first { - XCTAssertEqual(firstSubspecies["name"] as? String, "Subhuman", "encoding name") - XCTAssertEqual(firstSubspecies["plural"] as? String, "Subhumans", "encoding name") - XCTAssertEqual(firstSubspecies["lifespan"] as? Int, 45, "encoding lifespan") - XCTAssertNil(firstSubspecies["darkvision"], "encoding darkvision") - XCTAssertEqual(firstSubspecies["speed"] as? Int, 30, "encoding speed") - } else { - XCTFail("subspecies should be non-nil and contain at least one subspecies") - } - } - catch let error { - XCTFail("decode failed with error: \(error)") + let subspecies = try #require(dictionary["subspecies"] as? [[String: Any]]) + let firstSubspecies = try #require(subspecies.first) + #expect(firstSubspecies["name"] as? String == "Subhuman", "encoding name") + #expect(firstSubspecies["plural"] as? String == "Subhumans", "encoding plural") + #expect(firstSubspecies["lifespan"] as? Int == 45, "encoding lifespan") + #expect(firstSubspecies["darkvision"] == nil, "encoding darkvision should be nil") + #expect(firstSubspecies["speed"] as? Int == 30, "encoding speed") } + // Test 2: Subspecies with different overrides do { var copyOfSpeciesTraits = speciesTraits let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], descriptiveTraits: ["background": "Something"], lifespan: 45, darkVision: 10, speed: 45) @@ -229,18 +194,13 @@ class SpeciesTraitsTests: XCTestCase { let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as! [String: Any] // Confirm subspecies traits - if let subspecies = dictionary["subspecies"] as? [[String: Any]], let firstSubspecies = subspecies.first { - XCTAssertEqual(firstSubspecies["name"] as? String, "Subhuman", "encoding name") - XCTAssertEqual(firstSubspecies["plural"] as? String, "Subhumans", "encoding name") - XCTAssertEqual(firstSubspecies["lifespan"] as? Int, 45, "encoding lifespan") - XCTAssertEqual(firstSubspecies["darkvision"] as? Int, 10, "encoding darkvision") - XCTAssertNil(firstSubspecies["speed"], "encoding speed") - } else { - XCTFail("subspecies should be non-nil and contain at least one subspecies") - } - } - catch let error { - XCTFail("decode failed with error: \(error)") + let subspecies = try #require(dictionary["subspecies"] as? [[String: Any]]) + let firstSubspecies = try #require(subspecies.first) + #expect(firstSubspecies["name"] as? String == "Subhuman", "encoding name") + #expect(firstSubspecies["plural"] as? String == "Subhumans", "encoding plural") + #expect(firstSubspecies["lifespan"] as? Int == 45, "encoding lifespan") + #expect(firstSubspecies["darkvision"] as? Int == 10, "encoding darkvision") + #expect(firstSubspecies["speed"] == nil, "encoding speed should be nil") } } } diff --git a/RolePlayingCore/RolePlayingCoreTests/TestBundleClass.swift b/RolePlayingCore/RolePlayingCoreTests/TestBundleClass.swift new file mode 100644 index 0000000..b7f31b8 --- /dev/null +++ b/RolePlayingCore/RolePlayingCoreTests/TestBundleClass.swift @@ -0,0 +1,13 @@ +// +// TestBundleClass.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/29/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +import Foundation + +class TestBundleClass { } + +var testBundle = Bundle(for: TestBundleClass.self) diff --git a/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json b/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json index 8269a7f..161afa0 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestMoreSpecies.json @@ -29,26 +29,6 @@ }] }, { - "name": "Half-Elf", - "plural": "Half-Elves", - "lifespan": 180, - "speed": 30, - "darkvision": 60, - "skill proficiencies": ["fey ancestry"], - "languages": ["Common", "Elvish"], - "extra languages": 1, - "skill versatility": 2 - }, - { - "name": "Half-Orc", - "plural": "Half-Orcs", - "lifespan": 75, - "speed": 30, - "darkvision": 60, - "skill proficiencies": ["menacing", "relentless endurance", "savage attacks"], - "languages": ["Common", "Orcish"] - }, - { "name": "Tiefling", "plural": "Tieflings", "lifespan": 100, diff --git a/RolePlayingCore/RolePlayingCoreTests/TestSpeciesNames.json b/RolePlayingCore/RolePlayingCoreTests/TestSpeciesNames.json index 201f5b0..45e4d6b 100644 --- a/RolePlayingCore/RolePlayingCoreTests/TestSpeciesNames.json +++ b/RolePlayingCore/RolePlayingCoreTests/TestSpeciesNames.json @@ -40,12 +40,6 @@ "family": ["Beren", "Daergel", "Folkor", "Garrick", "Nackle", "Murning", "Ningel", "Raulnor", "Scheppen", "Timbers", "Turen"], "nickname": ["Aleslosh", "Ashhearth", "Badger", "Cloak", "Doublelock", "Filchbatter", "Fnipper", "Ku", "Nim", "Oneshoe", "Pock", "Sparklegem", "Stumbleduck"] }, - "Half-Elf": { - "aliases": ["Elf", "Human"] - }, - "Half-Orc": { - "aliases": ["Orc", "Human"] - }, "Orc": { "family type": "", "Male": ["Dench", "Feng", "Gell", "Henk", "Holg", "Imsh", "Keth", "Krusk", "Mhurren", "Ront", "Shump", "Thokk"], diff --git a/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift b/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift index 9fc85b6..9b22104 100644 --- a/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/WeightTests.swift @@ -6,46 +6,47 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class UnitWeightTests: XCTestCase { +@Suite("Weight Parsing and Serialization Tests") +struct UnitWeightTests { - func testWeights() { + @Test("Parse various weight formats") + func weights() async throws { do { let howHeavy = "70".parseWeight - XCTAssertNotNil(howHeavy, "weight should be non-nil") - XCTAssertEqual(howHeavy?.value, 70, "weight should be 3.0") + #expect(howHeavy != nil, "weight should be non-nil") + #expect(howHeavy?.value == 70, "weight should be 70") } do { let howHeavy = "3.0".parseWeight - XCTAssertNotNil(howHeavy, "weight should be non-nil") - XCTAssertEqual(howHeavy?.value, 3.0, "weight should be 3.0") + #expect(howHeavy != nil, "weight should be non-nil") + #expect(howHeavy?.value == 3.0, "weight should be 3.0") } do { let howHeavy = "45lb".parseWeight - XCTAssertNotNil(howHeavy, "weight should be non-nil") - XCTAssertEqual(howHeavy?.value, 45, "weight should be 45") + #expect(howHeavy != nil, "weight should be non-nil") + #expect(howHeavy?.value == 45, "weight should be 45") } do { let howHeavy = "174 kg".parseWeight - XCTAssertNotNil(howHeavy, "weight should be non-nil") - XCTAssertEqual(howHeavy?.value, 174, "weight should be 174") + #expect(howHeavy != nil, "weight should be non-nil") + #expect(howHeavy?.value == 174, "weight should be 174") } } - func testInvalidWeights() { - do { - let howHeavy = "99 hello".parseWeight - XCTAssertNil(howHeavy, "weight should be nil") - } + @Test("Parse invalid weight strings") + func invalidWeights() async throws { + let howHeavy = "99 hello".parseWeight + #expect(howHeavy == nil, "weight should be nil") } - func testEncodingWeight() { + @Test("Encode weight to JSON") + func encodingWeight() async throws { struct WeightContainer: Encodable { let weight: Weight @@ -61,21 +62,17 @@ class UnitWeightTests: XCTestCase { let weightContainer = WeightContainer(weight: Weight(value: 2.9, unit: .kilograms)) // Do a round-trip through serialization, then deserialization to confirm that it became a string - do { - let encoder = JSONEncoder() - let encoded = try encoder.encode(weightContainer) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) - print("deserialized = \n\(deserialized)") - let container = deserialized as? [String: Any] - let weight = container?["weight"] as? String - XCTAssertEqual(weight, "2.9 kg", "Encoded weight did not become a string") - } - catch let error { - XCTFail("Encoding weight threw an error: \(error)") - } + let encoder = JSONEncoder() + let encoded = try encoder.encode(weightContainer) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: .allowFragments) + print("deserialized = \n\(deserialized)") + let container = deserialized as? [String: Any] + let weight = container?["weight"] as? String + #expect(weight == "2.9 kg", "Encoded weight did not become a string") } - func testDecodingHeight() { + @Test("Decode weight from JSON") + func decodingWeight() async throws { struct WeightContainer: Decodable { let weight: Weight } @@ -86,14 +83,11 @@ class UnitWeightTests: XCTestCase { "weight": "147 lb" } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(WeightContainer.self, from: traits) - - XCTAssertEqual(decoded.weight.value, 147, "Decoded weight should be 147 lb") - } catch let error { - XCTFail("Decoding weight threw an error: \(error)") - } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(WeightContainer.self, from: traits) + + #expect(decoded.weight.value == 147, "Decoded weight should be 147 lb") } do { @@ -102,14 +96,11 @@ class UnitWeightTests: XCTestCase { "weight": 17 } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(WeightContainer.self, from: traits) - - XCTAssertEqual(decoded.weight.value, 17, "Decoded weight should be 17 lb") - } catch let error { - XCTFail("Decoding weight threw an error: \(error)") - } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(WeightContainer.self, from: traits) + + #expect(decoded.weight.value == 17, "Decoded weight should be 17 lb") } do { @@ -118,18 +109,16 @@ class UnitWeightTests: XCTestCase { "weight": "abcdefg" } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() + + let decoder = JSONDecoder() + #expect(throws: (any Error).self) { _ = try decoder.decode(WeightContainer.self, from: traits) - XCTFail("Decoding weight should have thrown an error") - - } catch let error { - print("Decoding invalid weight successfully threw an error: \(error)") } } } - func testDecodingWeightIfPresent() { + @Test("Decode optional weight from JSON") + func decodingWeightIfPresent() async throws { struct WeightContainer: Decodable { let weight: Weight? // The ? will trigger decodeIfPresent in the decoder } @@ -140,14 +129,11 @@ class UnitWeightTests: XCTestCase { "weight": "220 lb" } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(WeightContainer.self, from: traits) - - XCTAssertEqual(decoded.weight?.value, 220.0, "Decoded weight should be 220 lb") - } catch let error { - XCTFail("Decoding weight threw an error: \(error)") - } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(WeightContainer.self, from: traits) + + #expect(decoded.weight?.value == 220.0, "Decoded weight should be 220 lb") } do { @@ -156,14 +142,11 @@ class UnitWeightTests: XCTestCase { "weight": 6.5 } """.data(using: .utf8)! - do { - let decoder = JSONDecoder() - let decoded = try decoder.decode(WeightContainer.self, from: traits) - - XCTAssertEqual(decoded.weight?.value, 6.5, "Decoded weight should be 4 ft 3 in") - } catch let error { - XCTFail("Decoding weight threw an error: \(error)") - } + + let decoder = JSONDecoder() + let decoded = try decoder.decode(WeightContainer.self, from: traits) + + #expect(decoded.weight?.value == 6.5, "Decoded weight should be 6.5") } } } From 51c70566748988fa5971b4829c00a07abc4af885 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Wed, 29 Oct 2025 11:38:34 -0400 Subject: [PATCH 05/33] Split DiceParserTests into two files. --- .../RolePlayingCore.xcodeproj/project.pbxproj | 4 + .../DiceEncodingTests.swift | 175 ++++++++++++++++++ .../DiceParserTests.swift | 162 ---------------- 3 files changed, 179 insertions(+), 162 deletions(-) create mode 100644 RolePlayingCore/RolePlayingCoreTests/DiceEncodingTests.swift diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj index 6e9e845..6f190d0 100644 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj +++ b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj @@ -64,6 +64,7 @@ B6CF53EB1E574ED400CADD9F /* HalfBakedJSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */; }; B6D226F82EB25ABF00939968 /* TestBundleClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D226F72EB25AB900939968 /* TestBundleClass.swift */; }; B6D2EB1F26D7B7E900F99B35 /* RandomIndexGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */; }; + B6E18DF62EB26B9B000B3C90 /* DiceEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E18DF52EB26B96000B3C90 /* DiceEncodingTests.swift */; }; B6F070361E4F991D00F66918 /* AlignmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070351E4F991D00F66918 /* AlignmentTests.swift */; }; B6F070381E4FBD6500F66918 /* SpeciesTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */; }; B6F0703A1E4FC03700F66918 /* Money.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070391E4FC03700F66918 /* Money.swift */; }; @@ -155,6 +156,7 @@ B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HalfBakedJSONFile.json; sourceTree = ""; }; B6D226F72EB25AB900939968 /* TestBundleClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundleClass.swift; sourceTree = ""; }; B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomIndexGenerator.swift; sourceTree = ""; }; + B6E18DF52EB26B96000B3C90 /* DiceEncodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiceEncodingTests.swift; sourceTree = ""; }; B6F070351E4F991D00F66918 /* AlignmentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlignmentTests.swift; sourceTree = ""; }; B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeciesTraits.swift; sourceTree = ""; }; B6F070391E4FC03700F66918 /* Money.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Money.swift; sourceTree = ""; }; @@ -238,6 +240,7 @@ B6FA6CB51E47B080004D91B1 /* CurrencyTests.swift */, B62056091E19DDC0002494AB /* DiceTests.swift */, B62D89C31F09A3870095D587 /* DiceParserTests.swift */, + B6E18DF52EB26B96000B3C90 /* DiceEncodingTests.swift */, B6FA6CBE1E47C2F9004D91B1 /* HeightTests.swift */, B6FA6CC01E47C306004D91B1 /* WeightTests.swift */, B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */, @@ -537,6 +540,7 @@ B6FA6CC11E47C306004D91B1 /* WeightTests.swift in Sources */, B66AF9071EAE88FF00C15F8E /* ConfigurationTests.swift in Sources */, B69F846B1E58D33900A4D2B0 /* PlayerTests.swift in Sources */, + B6E18DF62EB26B9B000B3C90 /* DiceEncodingTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/RolePlayingCore/RolePlayingCoreTests/DiceEncodingTests.swift b/RolePlayingCore/RolePlayingCoreTests/DiceEncodingTests.swift new file mode 100644 index 0000000..7396f27 --- /dev/null +++ b/RolePlayingCore/RolePlayingCoreTests/DiceEncodingTests.swift @@ -0,0 +1,175 @@ +// +// DiceEncodingTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 10/29/25. +// Copyright © 2025 Brian Arnold. All rights reserved. +// + +import XCTest + +import RolePlayingCore + +class DiceEncodingTests: XCTestCase { + + func testEncodingDice() { + struct DiceContainer: Encodable { + let dice: Dice + + enum CodingKeys: String, CodingKey { + case dice + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("\(dice)", forKey: .dice) + } + } + + let diceContainer = DiceContainer(dice: CompoundDice(.d8, times: 3, modifier: 3, mathOperator: "-")) + let encoder = JSONEncoder() + do { + let encoded = try encoder.encode(diceContainer) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] + XCTAssertEqual(deserialized?["dice"], "3d8-3", "encoded dice failed to deserialize as string") + } + catch let error { + XCTFail("encoded dice failed, error: \(error)") + } + } + func testDecodingDice() { + struct DiceContainer: Decodable { + let dice: Dice + + enum CodingKeys: String, CodingKey { + case dice + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + dice = try container.decode(Dice.self, forKey: .dice) + } + } + + // Decode a typical Dice expression + do { + let traits = """ + { + "dice": "2d6+2" + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + do { + let decoded = try decoder.decode(DiceContainer.self, from: traits) + XCTAssertNotNil(decoded.dice as? CompoundDice, "decode as compound dice") + XCTAssertEqual(decoded.dice.sides, 6, "decode dice sides") + } + catch let error { + XCTFail("decoded dice failed, error: \(error)") + } + } + + // Decode a dice modifier + do { + let traits = """ + { + "dice": 5 + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + do { + let decoded = try decoder.decode(DiceContainer.self, from: traits) + XCTAssertNotNil(decoded.dice as? DiceModifier, "decode as compound dice") + XCTAssertEqual(decoded.dice.sides, 5, "decode dice sides") + } + catch let error { + XCTFail("decoded dice failed, error: \(error)") + } + } + + // Attempt to decode invalid dice + do { + let traits = """ + { + "dice": "Hello Dice" + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + do { + _ = try decoder.decode(DiceContainer.self, from: traits) + XCTFail("decode invalid dice string should have failed") + } + catch { + // Successfully errored + } + } + } + + func testDecodingDiceIfPresent() { + struct DiceContainer: Decodable { + let dice: Dice? + + enum CodingKeys: String, CodingKey { + case dice + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + dice = try container.decodeIfPresent(Dice.self, forKey: .dice) + } + } + + // Decode a typical Dice expression + do { + let traits = """ + { + "dice": "2d6+2" + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + do { + let decoded = try decoder.decode(DiceContainer.self, from: traits) + XCTAssertNotNil(decoded.dice as? CompoundDice, "decode as compound dice") + XCTAssertEqual(decoded.dice?.sides, 6, "decode dice sides") + } + catch let error { + XCTFail("decoded dice failed, error: \(error)") + } + } + + // Decode a dice modifier + do { + let traits = """ + { + "dice": 5 + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + do { + let decoded = try decoder.decode(DiceContainer.self, from: traits) + XCTAssertNotNil(decoded.dice as? DiceModifier, "decode as compound dice") + XCTAssertEqual(decoded.dice?.sides, 5, "decode dice sides") + } + catch let error { + XCTFail("decoded dice failed, error: \(error)") + } + } + + // Attempt to decode invalid dice + do { + let traits = """ + { + "dice": "Hello Dice" + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + do { + let decoded = try decoder.decode(DiceContainer.self, from: traits) + XCTAssertNil(decoded.dice, "Dice should have not been parsed") + } + catch let error { + XCTFail("decoded dice failed, error: \(error)") + } + } + } +} diff --git a/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift b/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift index 9bdc7f3..8366af8 100644 --- a/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift @@ -392,166 +392,4 @@ class DiceParserTests: XCTestCase { XCTAssertNil(roll, "'\(badFormatString)' consecutive dice expressions") } } - - func testDecodingDice() { - struct DiceContainer: Decodable { - let dice: Dice - - enum CodingKeys: String, CodingKey { - case dice - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - dice = try container.decode(Dice.self, forKey: .dice) - } - } - - // Decode a typical Dice expression - do { - let traits = """ - { - "dice": "2d6+2" - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNotNil(decoded.dice as? CompoundDice, "decode as compound dice") - XCTAssertEqual(decoded.dice.sides, 6, "decode dice sides") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") - } - } - - // Decode a dice modifier - do { - let traits = """ - { - "dice": 5 - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNotNil(decoded.dice as? DiceModifier, "decode as compound dice") - XCTAssertEqual(decoded.dice.sides, 5, "decode dice sides") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") - } - } - - // Attempt to decode invalid dice - do { - let traits = """ - { - "dice": "Hello Dice" - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - _ = try decoder.decode(DiceContainer.self, from: traits) - XCTFail("decode invalid dice string should have failed") - } - catch { - // Successfully errored - } - } - } - - func testDecodingDiceIfPresent() { - struct DiceContainer: Decodable { - let dice: Dice? - - enum CodingKeys: String, CodingKey { - case dice - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - dice = try container.decodeIfPresent(Dice.self, forKey: .dice) - } - } - - // Decode a typical Dice expression - do { - let traits = """ - { - "dice": "2d6+2" - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNotNil(decoded.dice as? CompoundDice, "decode as compound dice") - XCTAssertEqual(decoded.dice?.sides, 6, "decode dice sides") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") - } - } - - // Decode a dice modifier - do { - let traits = """ - { - "dice": 5 - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNotNil(decoded.dice as? DiceModifier, "decode as compound dice") - XCTAssertEqual(decoded.dice?.sides, 5, "decode dice sides") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") - } - } - - // Attempt to decode invalid dice - do { - let traits = """ - { - "dice": "Hello Dice" - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNil(decoded.dice, "Dice should have not been parsed") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") - } - } - } - - func testEncodingDice() { - struct DiceContainer: Encodable { - let dice: Dice - - enum CodingKeys: String, CodingKey { - case dice - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode("\(dice)", forKey: .dice) - } - } - - let diceContainer = DiceContainer(dice: CompoundDice(.d8, times: 3, modifier: 3, mathOperator: "-")) - let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(diceContainer) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] - XCTAssertEqual(deserialized?["dice"], "3d8-3", "encoded dice failed to deserialize as string") - } - catch let error { - XCTFail("encoded dice failed, error: \(error)") - } - } } From 1565833822e4cd8a3cf63a27cae2f9b81cc5da08 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Wed, 29 Oct 2025 11:40:50 -0400 Subject: [PATCH 06/33] Uncomment lock calls when encoding. --- RolePlayingCore/RolePlayingCore/Currency/Currencies.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift b/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift index de3a1b2..0155d21 100644 --- a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift +++ b/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift @@ -107,9 +107,9 @@ extension Currencies: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) -// Currencies.lock.lock() + Currencies.lock.lock() let allCurrenciesSnapshot = Currencies.allCurrencies.values -// Currencies.lock.unlock() + Currencies.lock.unlock() var currencies = [Currency]() for unitCurrency in allCurrenciesSnapshot { From e7c754715277f7958fc7905f1880baad5319e910 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Wed, 29 Oct 2025 11:48:52 -0400 Subject: [PATCH 07/33] Miscellaneous whitespace cleanup, and one more converted test. --- .../RolePlayingCoreTests/AlignmentTests.swift | 2 +- .../CharacterGeneratorTests.swift | 45 +++++++------------ .../ClassTraitsTests.swift | 1 - .../RolePlayingCoreTests/ClassesTests.swift | 1 - .../DiceEncodingTests.swift | 1 - .../DiceParserTests.swift | 1 - .../NameGeneratorTests.swift | 1 - .../SpeciesNamesTests.swift | 1 - 8 files changed, 18 insertions(+), 35 deletions(-) diff --git a/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift b/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift index 7084d6f..300548c 100644 --- a/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/AlignmentTests.swift @@ -6,9 +6,9 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import Foundation import Testing import RolePlayingCore +import Foundation @Suite("Alignment Tests") struct AlignmentTests { diff --git a/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift b/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift index 6382cca..4bc01da 100644 --- a/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/CharacterGeneratorTests.swift @@ -6,44 +6,33 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest - +import Testing import RolePlayingCore -class CharacterGeneratorTests: XCTestCase { +@Suite("Character Generator Tests") +struct CharacterGeneratorTests { let bundle = testBundle let sampleSize = 256 - func testCharacterGenerator() { - do { - let configuration = try Configuration("TestCharacterGenerator", from: bundle) - let characterGenerator = try CharacterGenerator(configuration, from: bundle) - - for _ in 0.. Date: Wed, 29 Oct 2025 12:01:45 -0400 Subject: [PATCH 08/33] Converted remaining dice tests to Swift Testing. --- .../DiceEncodingTests.swift | 215 +++++++------- .../DiceParserTests.swift | 280 ++++++++---------- 2 files changed, 233 insertions(+), 262 deletions(-) diff --git a/RolePlayingCore/RolePlayingCoreTests/DiceEncodingTests.swift b/RolePlayingCore/RolePlayingCoreTests/DiceEncodingTests.swift index 3a76095..fb0126c 100644 --- a/RolePlayingCore/RolePlayingCoreTests/DiceEncodingTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/DiceEncodingTests.swift @@ -6,12 +6,14 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // -import XCTest +import Testing import RolePlayingCore -class DiceEncodingTests: XCTestCase { +@Suite("Dice Encoding Tests") +struct DiceEncodingTests { - func testEncodingDice() { + @Test("Encoding dice") + func encodingDice() throws { struct DiceContainer: Encodable { let dice: Dice @@ -27,16 +29,13 @@ class DiceEncodingTests: XCTestCase { let diceContainer = DiceContainer(dice: CompoundDice(.d8, times: 3, modifier: 3, mathOperator: "-")) let encoder = JSONEncoder() - do { - let encoded = try encoder.encode(diceContainer) - let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] - XCTAssertEqual(deserialized?["dice"], "3d8-3", "encoded dice failed to deserialize as string") - } - catch let error { - XCTFail("encoded dice failed, error: \(error)") - } + let encoded = try encoder.encode(diceContainer) + let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] + #expect(deserialized?["dice"] == "3d8-3", "encoded dice failed to deserialize as string") } - func testDecodingDice() { + + @Test("Decoding dice - typical expression") + func decodingDiceTypicalExpression() throws { struct DiceContainer: Decodable { let dice: Dice @@ -50,61 +49,71 @@ class DiceEncodingTests: XCTestCase { } } - // Decode a typical Dice expression - do { - let traits = """ - { - "dice": "2d6+2" - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNotNil(decoded.dice as? CompoundDice, "decode as compound dice") - XCTAssertEqual(decoded.dice.sides, 6, "decode dice sides") + let traits = """ + { + "dice": "2d6+2" + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + let decoded = try decoder.decode(DiceContainer.self, from: traits) + #expect(decoded.dice is CompoundDice, "decode as compound dice") + #expect(decoded.dice.sides == 6, "decode dice sides") + } + + @Test("Decoding dice - dice modifier") + func decodingDiceDiceModifier() throws { + struct DiceContainer: Decodable { + let dice: Dice + + enum CodingKeys: String, CodingKey { + case dice } - catch let error { - XCTFail("decoded dice failed, error: \(error)") + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + dice = try container.decode(Dice.self, forKey: .dice) } } - // Decode a dice modifier - do { - let traits = """ - { - "dice": 5 - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNotNil(decoded.dice as? DiceModifier, "decode as compound dice") - XCTAssertEqual(decoded.dice.sides, 5, "decode dice sides") + let traits = """ + { + "dice": 5 + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + let decoded = try decoder.decode(DiceContainer.self, from: traits) + #expect(decoded.dice is DiceModifier, "decode as dice modifier") + #expect(decoded.dice.sides == 5, "decode dice sides") + } + + @Test("Decoding dice - invalid dice string") + func decodingDiceInvalidString() throws { + struct DiceContainer: Decodable { + let dice: Dice + + enum CodingKeys: String, CodingKey { + case dice } - catch let error { - XCTFail("decoded dice failed, error: \(error)") + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + dice = try container.decode(Dice.self, forKey: .dice) } } - // Attempt to decode invalid dice - do { - let traits = """ - { - "dice": "Hello Dice" - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - _ = try decoder.decode(DiceContainer.self, from: traits) - XCTFail("decode invalid dice string should have failed") - } - catch { - // Successfully errored - } + let traits = """ + { + "dice": "Hello Dice" + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + #expect(throws: Error.self) { + _ = try decoder.decode(DiceContainer.self, from: traits) } } - func testDecodingDiceIfPresent() { + @Test("Decoding dice if present - typical expression") + func decodingDiceIfPresentTypicalExpression() throws { struct DiceContainer: Decodable { let dice: Dice? @@ -118,57 +127,65 @@ class DiceEncodingTests: XCTestCase { } } - // Decode a typical Dice expression - do { - let traits = """ - { - "dice": "2d6+2" - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNotNil(decoded.dice as? CompoundDice, "decode as compound dice") - XCTAssertEqual(decoded.dice?.sides, 6, "decode dice sides") + let traits = """ + { + "dice": "2d6+2" + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + let decoded = try decoder.decode(DiceContainer.self, from: traits) + #expect(decoded.dice is CompoundDice, "decode as compound dice") + #expect(decoded.dice?.sides == 6, "decode dice sides") + } + + @Test("Decoding dice if present - dice modifier") + func decodingDiceIfPresentDiceModifier() throws { + struct DiceContainer: Decodable { + let dice: Dice? + + enum CodingKeys: String, CodingKey { + case dice } - catch let error { - XCTFail("decoded dice failed, error: \(error)") + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + dice = try container.decodeIfPresent(Dice.self, forKey: .dice) } } - // Decode a dice modifier - do { - let traits = """ - { - "dice": 5 - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNotNil(decoded.dice as? DiceModifier, "decode as compound dice") - XCTAssertEqual(decoded.dice?.sides, 5, "decode dice sides") + let traits = """ + { + "dice": 5 + } + """.data(using: .utf8)! + let decoder = JSONDecoder() + let decoded = try decoder.decode(DiceContainer.self, from: traits) + #expect(decoded.dice is DiceModifier, "decode as dice modifier") + #expect(decoded.dice?.sides == 5, "decode dice sides") + } + + @Test("Decoding dice if present - invalid dice string") + func decodingDiceIfPresentInvalidString() throws { + struct DiceContainer: Decodable { + let dice: Dice? + + enum CodingKeys: String, CodingKey { + case dice } - catch let error { - XCTFail("decoded dice failed, error: \(error)") + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + dice = try container.decodeIfPresent(Dice.self, forKey: .dice) } } - // Attempt to decode invalid dice - do { - let traits = """ - { - "dice": "Hello Dice" - } - """.data(using: .utf8)! - let decoder = JSONDecoder() - do { - let decoded = try decoder.decode(DiceContainer.self, from: traits) - XCTAssertNil(decoded.dice, "Dice should have not been parsed") - } - catch let error { - XCTFail("decoded dice failed, error: \(error)") - } + let traits = """ + { + "dice": "Hello Dice" } + """.data(using: .utf8)! + let decoder = JSONDecoder() + let decoded = try decoder.decode(DiceContainer.self, from: traits) + #expect(decoded.dice == nil, "Dice should have not been parsed") } } diff --git a/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift b/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift index 6bff377..1837ec7 100644 --- a/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift +++ b/RolePlayingCore/RolePlayingCoreTests/DiceParserTests.swift @@ -6,389 +6,343 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // -import XCTest +import Testing import RolePlayingCore -class DiceParserTests: XCTestCase { +@Suite("Dice Parser Tests") +struct DiceParserTests { - func testDiceFormatString() { + @Test("Dice format string") + func diceFormatString() { let formatString = "d12" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((1...12).contains(roll), "rolling \(formatString), got \(roll)") + #expect((1...12).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((6.0...7.0).contains(mean), "expected mean around 6.5, got \(mean)") + #expect((6.0...7.0).contains(mean), "expected mean around 6.5, got \(mean)") - XCTAssertEqual(minValue, 1, "min value") - XCTAssertEqual(maxValue, 12, "max value") + #expect(minValue == 1, "min value") + #expect(maxValue == 12, "max value") } - func testDiceTimesString() { + @Test("Dice times string") + func diceTimesString() { let formatString = "2d10" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((2...20).contains(roll), "rolling \(formatString), got \(roll)") + #expect((2...20).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") + #expect((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") // TODO: Because 2d10 produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 3, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 19, "max value") + #expect(minValue <= 3, "min value") + #expect(maxValue >= 19, "max value") } - func testDiceTimesCapitalized() { + @Test("Dice times capitalized") + func diceTimesCapitalized() { let formatString = "2D10" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((2...20).contains(roll), "rolling \(formatString), got \(roll)") + #expect((2...20).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") + #expect((10.0...12.0).contains(mean), "expected mean around 11.0, got \(mean)") // TODO: Because 2d10 produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 3, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 19, "max value") + #expect(minValue <= 3, "min value") + #expect(maxValue >= 19, "max value") } - func testDiceAddModifier() { + @Test("Dice add modifier") + func diceAddModifier() { let formatString = "1d20+4" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((5...24).contains(roll), "rolling \(formatString), got \(roll)") + #expect((5...24).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") + #expect((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") - XCTAssertEqual(minValue, 5, "min value") - XCTAssertEqual(maxValue, 24, "max value") + #expect(minValue == 5, "min value") + #expect(maxValue == 24, "max value") } - func testDicePercent() { + @Test("Dice percent") + func dicePercent() { let formatString = "d%" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((1...100).contains(roll), "rolling \(formatString), got \(roll)") + #expect((1...100).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((45.0...56.0).contains(mean), "expected mean around 50.5, got \(mean)") + #expect((45.0...56.0).contains(mean), "expected mean around 50.5, got \(mean)") /// With such a big range, we may not hit the absolute min/max for the specified sample size. - XCTAssertLessThanOrEqual(minValue, 2, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 99, "max value") + #expect(minValue <= 2, "min value") + #expect(maxValue >= 99, "max value") // Check that the description has the % if formatDice != nil { - XCTAssertEqual("\(formatDice!.description)", "d%", "% description") + #expect(formatDice!.description == "d%", "% description") } } - func testMultiplyWithX() { + @Test("Multiply with X") + func multiplyWithX() { let formatString = "2d4x10" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((20...80).contains(roll), "rolling \(formatString), got \(roll)") + #expect((20...80).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") + #expect((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") - XCTAssertEqual(minValue, 20, "min value") - XCTAssertEqual(maxValue, 80, "max value") + #expect(minValue == 20, "min value") + #expect(maxValue == 80, "max value") } - func testMultiplyWithAsterisk() { + @Test("Multiply with asterisk") + func multiplyWithAsterisk() { let formatString = "2d4*10" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((20...80).contains(roll), "rolling \(formatString), got \(roll)") + #expect((20...80).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") + #expect((46.0...56.0).contains(mean), "expected mean around 50.0, got \(mean)") - XCTAssertEqual(minValue, 20, "min value") - XCTAssertEqual(maxValue, 80, "max value") + #expect(minValue == 20, "min value") + #expect(maxValue == 80, "max value") } - func testDivide() { + @Test("Divide") + func divide() { let formatString = "d100/10" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((0...10).contains(roll), "rolling \(formatString), got \(roll)") + #expect((0...10).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((4.0...5.0).contains(mean), "expected mean around 4.5, got \(mean)") + #expect((4.0...5.0).contains(mean), "expected mean around 4.5, got \(mean)") - XCTAssertGreaterThanOrEqual(minValue, 0, "min value") - XCTAssertLessThanOrEqual(maxValue, 10, "max value") + #expect(minValue >= 0, "min value") + #expect(maxValue <= 10, "max value") } - func testDroppingLowest() { + @Test("Dropping lowest") + func droppingLowest() { let formatString = "4d6-L" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((3...18).contains(roll), "rolling \(formatString), got \(roll)") + #expect((3...18).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((11.0...13.5).contains(mean), "expected mean around 12.25, got \(mean)") + #expect((11.0...13.5).contains(mean), "expected mean around 12.25, got \(mean)") // TODO: Because 4x-L produces a sharp bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 5, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 16, "max value") + #expect(minValue <= 5, "min value") + #expect(maxValue >= 16, "max value") - XCTAssertEqual(formatDice?.sides, 6, "Dice sides") + #expect(formatDice?.sides == 6, "Dice sides") if let formatDice = formatDice { - XCTAssertEqual("\(formatDice.description)", "4d6-L", "SimpleDice description") + #expect(formatDice.description == "4d6-L", "SimpleDice description") } } - func testComplexDiceFormatString() { + @Test("Complex dice format string") + func complexDiceFormatString() { let formatString = "2d4+3d12-4" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((1...40).contains(roll), "rolling \(formatString), got \(roll)") + #expect((1...40).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((19.0...22.0).contains(mean), "expected mean around 20.5, got \(mean)") + #expect((19.0...22.0).contains(mean), "expected mean around 20.5, got \(mean)") // TODO: Because this produces a sharp bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 7, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 34, "max value") + #expect(minValue <= 7, "min value") + #expect(maxValue >= 34, "max value") - XCTAssertEqual(formatDice?.sides, 4, "Dice sides") + #expect(formatDice?.sides == 4, "Dice sides") if formatDice != nil { - XCTAssertEqual("\(formatDice!.description)", "2d4+3d12-4", "SimpleDice description") + #expect(formatDice!.description == "2d4+3d12-4", "SimpleDice description") } } - func testComplexDiceOperatorPrecedence() { + @Test("Complex dice operator precedence") + func complexDiceOperatorPrecedence() { let formatString = "2d4+d12-2+5" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((6...23).contains(roll), "rolling \(formatString), got \(roll)") + #expect((6...23).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") + #expect((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") // TODO: Because this produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 7, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 22, "max value") + #expect(minValue <= 7, "min value") + #expect(maxValue >= 22, "max value") - XCTAssertEqual(formatDice?.sides, 4, "Dice sides") + #expect(formatDice?.sides == 4, "Dice sides") if formatDice != nil { - XCTAssertEqual("\(formatDice!.description)", "2d4+d12-2+5", "SimpleDice description") + #expect(formatDice!.description == "2d4+d12-2+5", "SimpleDice description") } } - func testComplexDiceExtraRollDroppingWithWhitespace() { + @Test("Complex dice extra roll dropping with whitespace") + func complexDiceExtraRollDroppingWithWhitespace() { let formatString = "3d4- L + d12 -\n2 + 5" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should be non-nil") + #expect(formatDice != nil, "Dice from \(formatString) should be non-nil") var sum = 0 var minValue = 0 var maxValue = 0 for _ in 0 ..< sampleSize { let roll = formatDice?.roll().result ?? 0 - XCTAssertTrue((6...23).contains(roll), "rolling \(formatString), got \(roll)") + #expect((6...23).contains(roll), "rolling \(formatString), got \(roll)") sum += roll minValue = minValue == 0 ? roll : min(minValue, roll) maxValue = maxValue == 0 ? roll : max(maxValue, roll) } let mean = Double(sum)/Double(sampleSize) - XCTAssertTrue((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") + #expect((13.0...16.0).contains(mean), "expected mean around 14.5, got \(mean)") // TODO: Because this produces a bell curve, the actual min/max may be harder to get in a sample - XCTAssertLessThanOrEqual(minValue, 7, "min value") - XCTAssertGreaterThanOrEqual(maxValue, 22, "max value") + #expect(minValue <= 7, "min value") + #expect(maxValue >= 22, "max value") - XCTAssertEqual(formatDice?.sides, 4, "Dice sides") + #expect(formatDice?.sides == 4, "Dice sides") if formatDice != nil { - XCTAssertEqual("\(formatDice!.description)", "3d4-L+d12-2+5", "SimpleDice description") + #expect(formatDice!.description == "3d4-L+d12-2+5", "SimpleDice description") } } - func testConstantModifiers() { + @Test("Constant modifiers") + func constantModifiers() { let formatString = "1+3" let formatDice = formatString.parseDice - XCTAssertNotNil(formatDice, "Dice from \(formatString) should not be nil") + #expect(formatDice != nil, "Dice from \(formatString) should not be nil") if let formatDice = formatDice { - XCTAssertEqual(formatDice.description, "1+3", "format string") + #expect(formatDice.description == "1+3", "format string") let lastRoll = formatDice.roll() - XCTAssertEqual(lastRoll.description, "1 + 3", "format string") + #expect(lastRoll.description == "1 + 3", "format string") } } - func testDiceFormatStringNegative() { - // Negative tests - do { - let badFormatString = "d7" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' unsupported dice number") - } - - do { - let badFormatString = "dhello" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' unsupported dice number") - } - - do { - let badFormatString = "2+elephants" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' unsupported character tokens") - } - - // catch missing dice sides - do { - let badFormatString = "3d" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' missing dice sides") - - } - - // catch isDropping false code path at end of string, and missing expression - do { - let badFormatString = "2-" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' missing expression") - } - - // catch dropping missing minus - do { - let badFormatString = "2d4H" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' dropping missing minus") - } - - // catch dropping missing SimpleDice - do { - let badFormatString = "2-H" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' dropping missing SimpleDice") - } - - // catch consecutive numbers - do { - let badFormatString = "3 4" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' consecutive numbers") - } - - // catch consecutive math operators - do { - let badFormatString = "3++4" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' consecutive math operators") - } - - // Catch consecutive dice expressions (both valid dice) - do { - let badFormatString = "d4d4" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' consecutive dice expressions") - } - - // Catch consecutive dice 'd' characters - do { - let badFormatString = "dd4" - let roll = badFormatString.parseDice - XCTAssertNil(roll, "'\(badFormatString)' consecutive dice expressions") - } + @Test("Invalid dice format strings", arguments: [ + ("d7", "unsupported dice number"), + ("dhello", "unsupported dice number"), + ("2+elephants", "unsupported character tokens"), + ("3d", "missing dice sides"), + ("2-", "missing expression"), + ("2d4H", "dropping missing minus"), + ("2-H", "dropping missing SimpleDice"), + ("3 4", "consecutive numbers"), + ("3++4", "consecutive math operators"), + ("d4d4", "consecutive dice expressions"), + ("dd4", "consecutive dice 'd' characters") + ]) + func invalidDiceFormatStrings(badFormatString: String, reason: String) { + let roll = badFormatString.parseDice + #expect(roll == nil, "'\(badFormatString)' \(reason)") } } From 05bf3495f037313f378fe5eae9445f9bc02893db Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Wed, 29 Oct 2025 12:48:01 -0400 Subject: [PATCH 09/33] Replaced free licensed tshirt with system sf symbol tshirt. --- .../project.pbxproj | 4 +-- .../Assets.xcassets/Contents.json | 6 ++-- .../GenericPlayer.imageset/Contents.json | 21 ------------ .../sample-894-t-shirt@2x.png | Bin 717 -> 0 bytes .../GenericPlayer image license.txt | 31 ------------------ .../Player/PlayerListView.swift | 2 +- 6 files changed, 5 insertions(+), 59 deletions(-) delete mode 100644 CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/Contents.json delete mode 100644 CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/sample-894-t-shirt@2x.png delete mode 100644 CharacterGenerator/CharacterGenerator/GenericPlayer image license.txt diff --git a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj index ac82b46..561e9aa 100644 --- a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj +++ b/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj @@ -65,7 +65,6 @@ B621A3B91F0C1C7500E55236 /* CharacterGeneratorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorTests.swift; sourceTree = ""; }; B621A3BF1F0C1C7500E55236 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B621A3D61F0C1E3500E55236 /* GenericPlayer image license.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "GenericPlayer image license.txt"; sourceTree = ""; }; B626FA5E2EAE829300359F01 /* Backgrounds.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Backgrounds.json; sourceTree = ""; }; B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorApp.swift; sourceTree = ""; }; B64F68E12EA68686006D6C77 /* PlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListView.swift; sourceTree = ""; }; @@ -138,7 +137,6 @@ B6A1AE421F0C4383008ADF08 /* Configuration */, B621A3D71F0C1EE300E55236 /* Player */, B621A3AF1F0C1C7400E55236 /* Assets.xcassets */, - B621A3D61F0C1E3500E55236 /* GenericPlayer image license.txt */, B621A3B11F0C1C7400E55236 /* LaunchScreen.storyboard */, B621A3B41F0C1C7400E55236 /* Info.plist */, ); @@ -176,9 +174,9 @@ isa = PBXGroup; children = ( B6A1AE441F0C4383008ADF08 /* Configuration.json */, + B6A1AE451F0C4383008ADF08 /* Currencies.json */, B6A1AE431F0C4383008ADF08 /* Classes.json */, B626FA5E2EAE829300359F01 /* Backgrounds.json */, - B6A1AE451F0C4383008ADF08 /* Currencies.json */, B6A1AE461F0C4383008ADF08 /* Species.json */, B6A1AE4B1F0C5ABF008ADF08 /* SpeciesNames.json */, ); diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json b/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json +++ b/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/Contents.json b/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/Contents.json deleted file mode 100644 index 442af97..0000000 --- a/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "sample-894-t-shirt@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/sample-894-t-shirt@2x.png b/CharacterGenerator/CharacterGenerator/Assets.xcassets/GenericPlayer.imageset/sample-894-t-shirt@2x.png deleted file mode 100644 index f2286fffb86095eb941a73bc7da912f30babfe51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 717 zcmV;;0y6!HP) zzlsz=5XR^9A`^py!!r}{0gTk(B0WK$V!(GW5F>MAa}E7>K7fJ89{2D2T}g zYM|e5m>TA0duFz0dkYr2;M?x)uCA}Xs@c9}xuRIyHsx9h?}UE?IMm)$o9W2RR#u{fR(xaY(Q}PLhS|-rVI>{dz zw}R2bi)XdW83gWzU7FR&`At69P#499<}-mrp9`mjF~jGXz?s)$&aM50^Ih1n6?|xV zQJcR65`H1jojzj^AAElb7lc?F&EF~x$=GLXQSxhYG?8C5zb{-C&eUMe8hh~KJLO{1 zvBp9|s~i1;Tp)QU;FQWPDbO2%*h9bv-&BSMw7`kB;%@!sC2v+>*A?@zz|(PDgE?zA zgjElfcI9X7%KRs+0oX2dmpjqo>V826ritOUTF_4QQEb)V+GMd)b&UE(Q)8 zG<7xOWh9qU=nA#V1ub)SPAh)|yKF+j+9Kxyta!l$wsU9;waWz!XbBo$uL#8WzMgk0 zW<=U{HRJ6o{8MFvPxlEtk)%Dt7OY(%bF@8s?=^OLLqhTCyF;ox`Cz^khCa+)qUxZ- zOCPTZCFcWS?sAFEt&b=+POSeV-lf&XX6fyp1$tLJHDB&v>WX^D%N_Ktcxt}f!PFJ? zj+Z;=UGda>xr3=I>K!k4(7WQP`EmzSSJXRR?x1(YQ}g8xrmm=Wyxc+Wil^q|N7GpQ zrUN?yzf*eK7Z1W)BC&rSupeVR#?Rt~JIek8xPJu`Dvl}T00000NkvXXu0mjfnq5|2 diff --git a/CharacterGenerator/CharacterGenerator/GenericPlayer image license.txt b/CharacterGenerator/CharacterGenerator/GenericPlayer image license.txt deleted file mode 100644 index 8863864..0000000 --- a/CharacterGenerator/CharacterGenerator/GenericPlayer image license.txt +++ /dev/null @@ -1,31 +0,0 @@ -FREE ICONS FROM GLYPHISH -Created by Joseph Wain, 2014 -Web: http://glyphish.com or http://penandthink.com -Twitter: @glyphish or @jpwain - -Using these free icons requires attribution -- see below. It's easy and fun! - -Created by Joseph Wain and downloaded from http://glyphish.com - -This work is licensed under the Creative Commons Attribution 3.0 United States License. To view a copy, visit http://creativecommons.org/licenses/by/3.0/us/ - -You are free to share and to remix it remix under the following conditions: - - * You must attribute the work in the manner specified by the author (SEE BELOW). - - * For any reuse or distribution, you must make clear to others the license terms of this work. - - * The above conditions can be waived if you get permission from the copyright holder - (send me an email to discuss). - -You're free to use these icons for commercial and non-commercial purposes, for yourself, your company and your clients, and to edit, remix and otherwise modify them, as long as clear attribution is provided. You may not sell or redistribute the icons themselves as icons. - -Additionally, you may not use them in a way that encourages downstream distribution -- no templates or skins or theme kits or similar uses, no app-building tools or similar. (The person using the theme or template might not know where the icons came from and thus wouldn't be following the license.) This specifically prohibits the use in any kind of "app builder" tool or platform, especially where the icons are provided as choices to people using the tool to create apps. - -ATTRIBUTION -- A note on your app's website, like "Icons from Glyphish" or similar, plus a link back to glyphish.com, is the preferred form of attribution. For questions about attribution, contact me via the Glyphish website. - -USE WITHOUT ATTRIBUTION -- If attribution is difficult, unworkable or undesireable for your application, get in touch to discuss other options. Or, get the full Glyphish sets, which do not require attribution. - -Enjoy! - --Joseph \ No newline at end of file diff --git a/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift b/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift index a2c1147..fb4d6c0 100644 --- a/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift +++ b/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift @@ -56,7 +56,7 @@ struct PlayerRowView: View { var body: some View { HStack { - Image("GenericPlayer") + Image(systemName: "tshirt") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) From cbc736755dff9352ffb7319dd44e447f6615c944 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Thu, 30 Oct 2025 12:27:36 -0400 Subject: [PATCH 10/33] Refactored the library as a Swift Package. --- .gitignore | 12 + .../project.pbxproj | 670 --------------- .../AppIcon.appiconset/Contents.json | 98 --- .../Base.lproj/LaunchScreen.storyboard | 30 - .../CharacterGenerator/Info.plist | 53 -- .../CharacterGeneratorTests.swift | 28 - .../CharacterGeneratorTests/Info.plist | 22 - .../CharacterGeneratorUITests.swift | 43 - .../CharacterGeneratorUITests/Info.plist | 22 - .../contents.xcworkspacedata | 3 - .../project.pbxproj | 643 ++++++++++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 85 ++ .../Assets.xcassets/Contents.json | 0 .../CharacterGeneratorApp.swift | 1 + .../Configuration/Backgrounds.json | 0 .../Configuration/Classes.json | 0 .../Configuration/Configuration.json | 1 + .../Configuration/Currencies.json | 0 .../Configuration/Skills.json | 76 ++ .../Configuration/Species.json | 0 .../Configuration/SpeciesNames.json | 0 .../Player/CharacterSheet.swift | 2 +- .../Player/ExperiencePoints.swift | 0 .../Player/PlayerDetailView.swift | 0 .../Player/PlayerListView.swift | 0 .../CharacterGeneratorTests.swift | 16 + .../CharacterGeneratorUITests.swift | 41 + ...CharacterGeneratorUITestsLaunchTests.swift | 33 + Package.swift | 32 + README.md | 2 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../RolePlayingCore.xcodeproj/project.pbxproj | 806 ------------------ .../xcschemes/RolePlayingCore.xcscheme | 111 --- RolePlayingCore/RolePlayingCore/Info.plist | 24 - .../RolePlayingCore/Player/RacialNames.swift | 116 --- .../RolePlayingCore/Player/Skill.swift | 71 -- .../RolePlayingCore/RolePlayingCore.h | 19 - .../RolePlayingCoreTests/Info.plist | 22 - .../RacialNamesTests.swift | 55 -- .../RolePlayingCore/Common/Height.swift | 0 .../RolePlayingCore/Common/JSONFile.swift | 1 + .../Common/NameGenerator.swift | 0 .../Common/RandomIndexGenerator.swift | 0 .../RolePlayingCore/Common/ServiceError.swift | 0 .../RolePlayingCore/Common/SpeciesNames.swift | 0 .../RolePlayingCore/Common/Weight.swift | 0 .../Configuration/CharacterGenerator.swift | 0 .../Configuration/Configuration.swift | 10 +- .../RolePlayingCore/Currency/Currencies.swift | 17 +- .../RolePlayingCore/Currency/Money.swift | 0 .../Currency/UnitCurrency.swift | 7 +- .../RolePlayingCore/Dice/CompoundDice.swift | 6 +- .../RolePlayingCore/Dice/Dice.swift | 0 .../RolePlayingCore/Dice/DiceModifier.swift | 0 .../RolePlayingCore/Dice/DiceParser.swift | 2 +- .../RolePlayingCore/Dice/DiceRoll.swift | 0 .../RolePlayingCore/Dice/Die.swift | 0 .../RolePlayingCore/Dice/DroppingDice.swift | 0 .../RolePlayingCore/Dice/SimpleDice.swift | 0 .../RolePlayingCore/Player/Ability.swift | 2 +- .../RolePlayingCore/Player/Alignment.swift | 0 .../Player/BackgroundTraits.swift | 18 +- .../RolePlayingCore/Player/Backgrounds.swift | 0 .../RolePlayingCore/Player/ClassTraits.swift | 19 +- .../RolePlayingCore/Player/Classes.swift | 0 .../RolePlayingCore/Player/CreatureSize.swift | 0 .../RolePlayingCore/Player/Initiative.swift | 0 .../RolePlayingCore/Player/Player.swift | 18 +- .../RolePlayingCore/Player/Players.swift | 7 +- Sources/RolePlayingCore/Player/Skills.swift | 61 ++ .../RolePlayingCore/Player/Species.swift | 0 .../Player/SpeciesTraits.swift | 2 +- .../RolePlayingCoreTests/AbilityTests.swift | 2 +- .../RolePlayingCoreTests/AlignmentTests.swift | 0 .../BackgroundsTests.swift | 8 +- .../CharacterGeneratorTests.swift | 3 +- .../ClassTraitsTests.swift | 10 +- .../RolePlayingCoreTests/ClassesTests.swift | 3 +- .../ConfigurationTests.swift | 3 +- .../RolePlayingCoreTests/CurrencyTests.swift | 3 +- .../DiceEncodingTests.swift | 1 + .../DiceParserTests.swift | 0 .../RolePlayingCoreTests/DiceTests.swift | 0 .../RolePlayingCoreTests/HeightTests.swift | 1 + .../RolePlayingCoreTests/JSONFileTests.swift | 3 + .../NameGeneratorTests.swift | 3 +- .../RolePlayingCoreTests/PlayerTests.swift | 99 ++- .../RolePlayingCoreTests/PlayersTests.swift | 9 +- .../ServiceErrorTests.swift | 0 .../SpeciesNamesTests.swift | 3 +- .../RolePlayingCoreTests/SpeciesTests.swift | 3 +- .../SpeciesTraitsTests.swift | 1 + .../TestBundleClass.swift | 2 - .../TestResources}/HalfBakedJSONFile.json | 0 .../TestResources}/InvalidClassPlayers.json | 0 .../TestResources}/InvalidConfiguration.json | 0 .../TestResources}/InvalidJSONFile.json | 0 .../TestResources}/InvalidSpeciesPlayers.json | 0 .../TestResources}/JSONFile.json | 0 .../TestResources}/MissingClassPlayers.json | 0 .../TestResources}/MissingSpeciesPlayers.json | 0 .../TestResources}/TestBackgrounds.json | 0 .../TestCharacterGenerator.json | 1 + .../TestResources}/TestClasses.json | 0 .../TestResources}/TestConfiguration.json | 1 + .../TestResources}/TestCurrencies.json | 0 .../TestResources}/TestMoreClasses.json | 0 .../TestResources}/TestMoreSpecies.json | 0 .../TestResources}/TestNames.json | 0 .../TestResources}/TestPlayers.json | 4 +- .../TestResources/TestSkills.json | 77 ++ .../TestResources}/TestSpecies.json | 0 .../TestResources}/TestSpeciesNames.json | 0 .../RolePlayingCoreTests/WeightTests.swift | 1 + 115 files changed, 1297 insertions(+), 2270 deletions(-) delete mode 100644 CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj delete mode 100644 CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 CharacterGenerator/CharacterGenerator/Base.lproj/LaunchScreen.storyboard delete mode 100644 CharacterGenerator/CharacterGenerator/Info.plist delete mode 100644 CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift delete mode 100644 CharacterGenerator/CharacterGeneratorTests/Info.plist delete mode 100644 CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift delete mode 100644 CharacterGenerator/CharacterGeneratorUITests/Info.plist rename {RolePlayingCore.xcworkspace => Examples/CharacterGenerator/CharacterGenerator.xcworkspace}/contents.xcworkspacedata (66%) create mode 100644 Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj create mode 100644 Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Assets.xcassets/Contents.json (100%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/CharacterGeneratorApp.swift (99%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Configuration/Backgrounds.json (100%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Configuration/Classes.json (100%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Configuration/Configuration.json (86%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Configuration/Currencies.json (100%) create mode 100644 Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Skills.json rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Configuration/Species.json (100%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Configuration/SpeciesNames.json (100%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Player/CharacterSheet.swift (98%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Player/ExperiencePoints.swift (100%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Player/PlayerDetailView.swift (100%) rename {CharacterGenerator => Examples/CharacterGenerator/CharacterGenerator}/CharacterGenerator/Player/PlayerListView.swift (100%) create mode 100644 Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift create mode 100644 Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift create mode 100644 Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITestsLaunchTests.swift create mode 100644 Package.swift delete mode 100644 RolePlayingCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj delete mode 100644 RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme delete mode 100644 RolePlayingCore/RolePlayingCore/Info.plist delete mode 100644 RolePlayingCore/RolePlayingCore/Player/RacialNames.swift delete mode 100644 RolePlayingCore/RolePlayingCore/Player/Skill.swift delete mode 100644 RolePlayingCore/RolePlayingCore/RolePlayingCore.h delete mode 100644 RolePlayingCore/RolePlayingCoreTests/Info.plist delete mode 100644 RolePlayingCore/RolePlayingCoreTests/RacialNamesTests.swift rename {RolePlayingCore => Sources}/RolePlayingCore/Common/Height.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Common/JSONFile.swift (97%) rename {RolePlayingCore => Sources}/RolePlayingCore/Common/NameGenerator.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Common/RandomIndexGenerator.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Common/ServiceError.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Common/SpeciesNames.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Common/Weight.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Configuration/CharacterGenerator.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Configuration/Configuration.swift (93%) rename {RolePlayingCore => Sources}/RolePlayingCore/Currency/Currencies.swift (86%) rename {RolePlayingCore => Sources}/RolePlayingCore/Currency/Money.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Currency/UnitCurrency.swift (79%) rename {RolePlayingCore => Sources}/RolePlayingCore/Dice/CompoundDice.swift (90%) rename {RolePlayingCore => Sources}/RolePlayingCore/Dice/Dice.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Dice/DiceModifier.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Dice/DiceParser.swift (99%) rename {RolePlayingCore => Sources}/RolePlayingCore/Dice/DiceRoll.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Dice/Die.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Dice/DroppingDice.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Dice/SimpleDice.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/Ability.swift (99%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/Alignment.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/BackgroundTraits.swift (75%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/Backgrounds.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/ClassTraits.swift (93%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/Classes.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/CreatureSize.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/Initiative.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/Player.swift (95%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/Players.swift (94%) create mode 100644 Sources/RolePlayingCore/Player/Skills.swift rename {RolePlayingCore => Sources}/RolePlayingCore/Player/Species.swift (100%) rename {RolePlayingCore => Sources}/RolePlayingCore/Player/SpeciesTraits.swift (99%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/AbilityTests.swift (99%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/AlignmentTests.swift (100%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/BackgroundsTests.swift (94%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/CharacterGeneratorTests.swift (95%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/ClassTraitsTests.swift (97%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/ClassesTests.swift (96%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/ConfigurationTests.swift (96%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/CurrencyTests.swift (99%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/DiceEncodingTests.swift (99%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/DiceParserTests.swift (100%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/DiceTests.swift (100%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/HeightTests.swift (99%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/JSONFileTests.swift (97%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/NameGeneratorTests.swift (96%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/PlayerTests.swift (91%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/PlayersTests.swift (93%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/ServiceErrorTests.swift (100%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/SpeciesNamesTests.swift (97%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/SpeciesTests.swift (96%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/SpeciesTraitsTests.swift (99%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/TestBundleClass.swift (79%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/HalfBakedJSONFile.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/InvalidClassPlayers.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/InvalidConfiguration.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/InvalidJSONFile.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/InvalidSpeciesPlayers.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/JSONFile.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/MissingClassPlayers.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/MissingSpeciesPlayers.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestBackgrounds.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestCharacterGenerator.json (89%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestClasses.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestConfiguration.json (87%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestCurrencies.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestMoreClasses.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestMoreSpecies.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestNames.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestPlayers.json (90%) create mode 100644 Tests/RolePlayingCoreTests/TestResources/TestSkills.json rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestSpecies.json (100%) rename {RolePlayingCore/RolePlayingCoreTests => Tests/RolePlayingCoreTests/TestResources}/TestSpeciesNames.json (100%) rename {RolePlayingCore => Tests}/RolePlayingCoreTests/WeightTests.swift (99%) diff --git a/.gitignore b/.gitignore index 47042c2..8e1c0bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,14 @@ .DS_Store xcuserdata +DerivedData +.swiftpm/xcode/package.xcworkspace/ +.swiftpm/xcode/xcuserdata/ + +# Build artifacts +.build/ + +# Other common ignores +*.swp +*.bak +*.orig +*.lock \ No newline at end of file diff --git a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj deleted file mode 100644 index 561e9aa..0000000 --- a/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj +++ /dev/null @@ -1,670 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - B621A3B01F0C1C7400E55236 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B621A3AF1F0C1C7400E55236 /* Assets.xcassets */; }; - B621A3B31F0C1C7400E55236 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B621A3B11F0C1C7400E55236 /* LaunchScreen.storyboard */; }; - B621A3BE1F0C1C7500E55236 /* CharacterGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */; }; - B626FA5F2EAE829D00359F01 /* Backgrounds.json in Resources */ = {isa = PBXBuildFile; fileRef = B626FA5E2EAE829300359F01 /* Backgrounds.json */; }; - B64F68E02EA6867B006D6C77 /* CharacterGeneratorApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */; }; - B64F68E22EA68686006D6C77 /* PlayerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68E12EA68686006D6C77 /* PlayerListView.swift */; }; - B64F68E42EA6868A006D6C77 /* PlayerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64F68E32EA6868A006D6C77 /* PlayerDetailView.swift */; }; - B6688B4F2EABCB11000A83DD /* ExperiencePoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6688B4E2EABCB11000A83DD /* ExperiencePoints.swift */; }; - B6A1AE471F0C441E008ADF08 /* Classes.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE431F0C4383008ADF08 /* Classes.json */; }; - B6A1AE481F0C4421008ADF08 /* Configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE441F0C4383008ADF08 /* Configuration.json */; }; - B6A1AE491F0C4426008ADF08 /* Currencies.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE451F0C4383008ADF08 /* Currencies.json */; }; - B6A1AE4A1F0C4429008ADF08 /* Species.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE461F0C4383008ADF08 /* Species.json */; }; - B6A1AE4C1F0C5ABF008ADF08 /* SpeciesNames.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE4B1F0C5ABF008ADF08 /* SpeciesNames.json */; }; - B6A1AE501F0D6594008ADF08 /* CharacterSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A1AE4F1F0D6594008ADF08 /* CharacterSheet.swift */; }; - B6C87F431F83CD3E007CB209 /* CharacterGeneratorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C87F421F83CD3E007CB209 /* CharacterGeneratorUITests.swift */; }; - B6D1DB5520DD4A8100B402AC /* RolePlayingCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6D1DB5420DD4A8100B402AC /* RolePlayingCore.framework */; }; - B6D1DB5620DD4A8100B402AC /* RolePlayingCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B6D1DB5420DD4A8100B402AC /* RolePlayingCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - B621A3BA1F0C1C7500E55236 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B621A39B1F0C1C7400E55236 /* Project object */; - proxyType = 1; - remoteGlobalIDString = B621A3A21F0C1C7400E55236; - remoteInfo = CharacterGenerator; - }; - B6C87F451F83CD3E007CB209 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B621A39B1F0C1C7400E55236 /* Project object */; - proxyType = 1; - remoteGlobalIDString = B621A3A21F0C1C7400E55236; - remoteInfo = CharacterGenerator; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - B6ADA2A820C1D97F00012B7C /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - B6D1DB5620DD4A8100B402AC /* RolePlayingCore.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - B621A3A31F0C1C7400E55236 /* CharacterGenerator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CharacterGenerator.app; sourceTree = BUILT_PRODUCTS_DIR; }; - B621A3AF1F0C1C7400E55236 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - B621A3B21F0C1C7400E55236 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - B621A3B41F0C1C7400E55236 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B621A3B91F0C1C7500E55236 /* CharacterGeneratorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorTests.swift; sourceTree = ""; }; - B621A3BF1F0C1C7500E55236 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B626FA5E2EAE829300359F01 /* Backgrounds.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Backgrounds.json; sourceTree = ""; }; - B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorApp.swift; sourceTree = ""; }; - B64F68E12EA68686006D6C77 /* PlayerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListView.swift; sourceTree = ""; }; - B64F68E32EA6868A006D6C77 /* PlayerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDetailView.swift; sourceTree = ""; }; - B6688B4E2EABCB11000A83DD /* ExperiencePoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiencePoints.swift; sourceTree = ""; }; - B6A1AE431F0C4383008ADF08 /* Classes.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Classes.json; sourceTree = ""; }; - B6A1AE441F0C4383008ADF08 /* Configuration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Configuration.json; sourceTree = ""; }; - B6A1AE451F0C4383008ADF08 /* Currencies.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Currencies.json; sourceTree = ""; }; - B6A1AE461F0C4383008ADF08 /* Species.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Species.json; sourceTree = ""; }; - B6A1AE4B1F0C5ABF008ADF08 /* SpeciesNames.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SpeciesNames.json; sourceTree = ""; }; - B6A1AE4F1F0D6594008ADF08 /* CharacterSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterSheet.swift; sourceTree = ""; }; - B6C87F401F83CD3E007CB209 /* CharacterGeneratorUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B6C87F421F83CD3E007CB209 /* CharacterGeneratorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorUITests.swift; sourceTree = ""; }; - B6C87F441F83CD3E007CB209 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B6D1DB5420DD4A8100B402AC /* RolePlayingCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RolePlayingCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B621A3A01F0C1C7400E55236 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B6D1DB5520DD4A8100B402AC /* RolePlayingCore.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B621A3B61F0C1C7500E55236 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B6C87F3D1F83CD3E007CB209 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - B621A39A1F0C1C7400E55236 = { - isa = PBXGroup; - children = ( - B621A3A51F0C1C7400E55236 /* CharacterGenerator */, - B621A3BC1F0C1C7500E55236 /* CharacterGeneratorTests */, - B6C87F411F83CD3E007CB209 /* CharacterGeneratorUITests */, - B621A3A41F0C1C7400E55236 /* Products */, - B6D1DB5420DD4A8100B402AC /* RolePlayingCore.framework */, - B6A1AE3F1F0C3702008ADF08 /* Frameworks */, - ); - sourceTree = ""; - }; - B621A3A41F0C1C7400E55236 /* Products */ = { - isa = PBXGroup; - children = ( - B621A3A31F0C1C7400E55236 /* CharacterGenerator.app */, - B621A3B91F0C1C7500E55236 /* CharacterGeneratorTests.xctest */, - B6C87F401F83CD3E007CB209 /* CharacterGeneratorUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - B621A3A51F0C1C7400E55236 /* CharacterGenerator */ = { - isa = PBXGroup; - children = ( - B64F68DF2EA6867B006D6C77 /* CharacterGeneratorApp.swift */, - B6A1AE421F0C4383008ADF08 /* Configuration */, - B621A3D71F0C1EE300E55236 /* Player */, - B621A3AF1F0C1C7400E55236 /* Assets.xcassets */, - B621A3B11F0C1C7400E55236 /* LaunchScreen.storyboard */, - B621A3B41F0C1C7400E55236 /* Info.plist */, - ); - path = CharacterGenerator; - sourceTree = ""; - }; - B621A3BC1F0C1C7500E55236 /* CharacterGeneratorTests */ = { - isa = PBXGroup; - children = ( - B621A3BD1F0C1C7500E55236 /* CharacterGeneratorTests.swift */, - B621A3BF1F0C1C7500E55236 /* Info.plist */, - ); - path = CharacterGeneratorTests; - sourceTree = ""; - }; - B621A3D71F0C1EE300E55236 /* Player */ = { - isa = PBXGroup; - children = ( - B64F68E12EA68686006D6C77 /* PlayerListView.swift */, - B64F68E32EA6868A006D6C77 /* PlayerDetailView.swift */, - B6A1AE4F1F0D6594008ADF08 /* CharacterSheet.swift */, - B6688B4E2EABCB11000A83DD /* ExperiencePoints.swift */, - ); - path = Player; - sourceTree = ""; - }; - B6A1AE3F1F0C3702008ADF08 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - B6A1AE421F0C4383008ADF08 /* Configuration */ = { - isa = PBXGroup; - children = ( - B6A1AE441F0C4383008ADF08 /* Configuration.json */, - B6A1AE451F0C4383008ADF08 /* Currencies.json */, - B6A1AE431F0C4383008ADF08 /* Classes.json */, - B626FA5E2EAE829300359F01 /* Backgrounds.json */, - B6A1AE461F0C4383008ADF08 /* Species.json */, - B6A1AE4B1F0C5ABF008ADF08 /* SpeciesNames.json */, - ); - path = Configuration; - sourceTree = ""; - }; - B6C87F411F83CD3E007CB209 /* CharacterGeneratorUITests */ = { - isa = PBXGroup; - children = ( - B6C87F421F83CD3E007CB209 /* CharacterGeneratorUITests.swift */, - B6C87F441F83CD3E007CB209 /* Info.plist */, - ); - path = CharacterGeneratorUITests; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - B621A3A21F0C1C7400E55236 /* CharacterGenerator */ = { - isa = PBXNativeTarget; - buildConfigurationList = B621A3CD1F0C1C7500E55236 /* Build configuration list for PBXNativeTarget "CharacterGenerator" */; - buildPhases = ( - B621A39F1F0C1C7400E55236 /* Sources */, - B621A3A01F0C1C7400E55236 /* Frameworks */, - B621A3A11F0C1C7400E55236 /* Resources */, - B6ADA2A820C1D97F00012B7C /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = CharacterGenerator; - productName = CharacterGenerator; - productReference = B621A3A31F0C1C7400E55236 /* CharacterGenerator.app */; - productType = "com.apple.product-type.application"; - }; - B621A3B81F0C1C7500E55236 /* CharacterGeneratorTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = B621A3D01F0C1C7500E55236 /* Build configuration list for PBXNativeTarget "CharacterGeneratorTests" */; - buildPhases = ( - B621A3B51F0C1C7500E55236 /* Sources */, - B621A3B61F0C1C7500E55236 /* Frameworks */, - B621A3B71F0C1C7500E55236 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - B621A3BB1F0C1C7500E55236 /* PBXTargetDependency */, - ); - name = CharacterGeneratorTests; - productName = CharacterGeneratorTests; - productReference = B621A3B91F0C1C7500E55236 /* CharacterGeneratorTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - B6C87F3F1F83CD3E007CB209 /* CharacterGeneratorUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = B6C87F491F83CD3E007CB209 /* Build configuration list for PBXNativeTarget "CharacterGeneratorUITests" */; - buildPhases = ( - B6C87F3C1F83CD3E007CB209 /* Sources */, - B6C87F3D1F83CD3E007CB209 /* Frameworks */, - B6C87F3E1F83CD3E007CB209 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - B6C87F461F83CD3E007CB209 /* PBXTargetDependency */, - ); - name = CharacterGeneratorUITests; - productName = CharacterGeneratorUITests; - productReference = B6C87F401F83CD3E007CB209 /* CharacterGeneratorUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - B621A39B1F0C1C7400E55236 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0900; - LastUpgradeCheck = 1620; - ORGANIZATIONNAME = "Brian Arnold"; - TargetAttributes = { - B621A3A21F0C1C7400E55236 = { - CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1020; - }; - B621A3B81F0C1C7500E55236 = { - CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1020; - TestTargetID = B621A3A21F0C1C7400E55236; - }; - B6C87F3F1F83CD3E007CB209 = { - CreatedOnToolsVersion = 9.0; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - TestTargetID = B621A3A21F0C1C7400E55236; - }; - }; - }; - buildConfigurationList = B621A39E1F0C1C7400E55236 /* Build configuration list for PBXProject "CharacterGenerator" */; - compatibilityVersion = "Xcode 8.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = B621A39A1F0C1C7400E55236; - productRefGroup = B621A3A41F0C1C7400E55236 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - B621A3A21F0C1C7400E55236 /* CharacterGenerator */, - B621A3B81F0C1C7500E55236 /* CharacterGeneratorTests */, - B6C87F3F1F83CD3E007CB209 /* CharacterGeneratorUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - B621A3A11F0C1C7400E55236 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6A1AE481F0C4421008ADF08 /* Configuration.json in Resources */, - B6A1AE4A1F0C4429008ADF08 /* Species.json in Resources */, - B621A3B31F0C1C7400E55236 /* LaunchScreen.storyboard in Resources */, - B626FA5F2EAE829D00359F01 /* Backgrounds.json in Resources */, - B6A1AE491F0C4426008ADF08 /* Currencies.json in Resources */, - B621A3B01F0C1C7400E55236 /* Assets.xcassets in Resources */, - B6A1AE471F0C441E008ADF08 /* Classes.json in Resources */, - B6A1AE4C1F0C5ABF008ADF08 /* SpeciesNames.json in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B621A3B71F0C1C7500E55236 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B6C87F3E1F83CD3E007CB209 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - B621A39F1F0C1C7400E55236 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B64F68E42EA6868A006D6C77 /* PlayerDetailView.swift in Sources */, - B6A1AE501F0D6594008ADF08 /* CharacterSheet.swift in Sources */, - B64F68E22EA68686006D6C77 /* PlayerListView.swift in Sources */, - B64F68E02EA6867B006D6C77 /* CharacterGeneratorApp.swift in Sources */, - B6688B4F2EABCB11000A83DD /* ExperiencePoints.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B621A3B51F0C1C7500E55236 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B621A3BE1F0C1C7500E55236 /* CharacterGeneratorTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B6C87F3C1F83CD3E007CB209 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6C87F431F83CD3E007CB209 /* CharacterGeneratorUITests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - B621A3BB1F0C1C7500E55236 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = B621A3A21F0C1C7400E55236 /* CharacterGenerator */; - targetProxy = B621A3BA1F0C1C7500E55236 /* PBXContainerItemProxy */; - }; - B6C87F461F83CD3E007CB209 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = B621A3A21F0C1C7400E55236 /* CharacterGenerator */; - targetProxy = B6C87F451F83CD3E007CB209 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - B621A3B11F0C1C7400E55236 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - B621A3B21F0C1C7400E55236 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - B621A3CB1F0C1C7500E55236 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 6.0; - }; - name = Debug; - }; - B621A3CC1F0C1C7500E55236 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 6.0; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - B621A3CE1F0C1C7500E55236 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGenerator/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - B621A3CF1F0C1C7500E55236 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGenerator/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - B621A3D11F0C1C7500E55236 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGeneratorTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CharacterGenerator.app/CharacterGenerator"; - }; - name = Debug; - }; - B621A3D21F0C1C7500E55236 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGeneratorTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CharacterGenerator.app/CharacterGenerator"; - }; - name = Release; - }; - B6C87F471F83CD3E007CB209 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGeneratorUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = CharacterGenerator; - }; - name = Debug; - }; - B6C87F481F83CD3E007CB209 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = CharacterGeneratorUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = CharacterGenerator; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - B621A39E1F0C1C7400E55236 /* Build configuration list for PBXProject "CharacterGenerator" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B621A3CB1F0C1C7500E55236 /* Debug */, - B621A3CC1F0C1C7500E55236 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B621A3CD1F0C1C7500E55236 /* Build configuration list for PBXNativeTarget "CharacterGenerator" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B621A3CE1F0C1C7500E55236 /* Debug */, - B621A3CF1F0C1C7500E55236 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B621A3D01F0C1C7500E55236 /* Build configuration list for PBXNativeTarget "CharacterGeneratorTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B621A3D11F0C1C7500E55236 /* Debug */, - B621A3D21F0C1C7500E55236 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B6C87F491F83CD3E007CB209 /* Build configuration list for PBXNativeTarget "CharacterGeneratorUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B6C87F471F83CD3E007CB209 /* Debug */, - B6C87F481F83CD3E007CB209 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = B621A39B1F0C1C7400E55236 /* Project object */; -} diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json b/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d8db8d6..0000000 --- a/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" - }, - { - "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" - }, - { - "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/CharacterGenerator/CharacterGenerator/Base.lproj/LaunchScreen.storyboard b/CharacterGenerator/CharacterGenerator/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 6514e06..0000000 --- a/CharacterGenerator/CharacterGenerator/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CharacterGenerator/CharacterGenerator/Info.plist b/CharacterGenerator/CharacterGenerator/Info.plist deleted file mode 100644 index 4decacf..0000000 --- a/CharacterGenerator/CharacterGenerator/Info.plist +++ /dev/null @@ -1,53 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UIStatusBarTintParameters - - UINavigationBar - - Style - UIBarStyleDefault - Translucent - - - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift b/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift deleted file mode 100644 index 41d94e8..0000000 --- a/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// CharacterGeneratorTests.swift -// CharacterGeneratorTests -// -// Created by Brian Arnold on 7/4/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest -@testable import CharacterGenerator - -class CharacterGeneratorTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testCharacterGenerator() { - // TODO: there isn't much logic to test, since we use RolePlayingCore, which has its own tests. - } - -} diff --git a/CharacterGenerator/CharacterGeneratorTests/Info.plist b/CharacterGenerator/CharacterGeneratorTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/CharacterGenerator/CharacterGeneratorTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift b/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift deleted file mode 100644 index e54248f..0000000 --- a/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// CharacterGeneratorUITests.swift -// CharacterGeneratorUITests -// -// Created by Brian Arnold on 7/4/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -class CharacterGeneratorUITests: XCTestCase { - - override func setUp() { - super.setUp() - - continueAfterFailure = false - - XCUIApplication().launch() - } - - override func tearDown() { - // TODO: reset state once we have more state - - super.tearDown() - } - - func testAddingAndShowingDetail() { - let app = XCUIApplication() - let playersNavigationBar = app.navigationBars["Players"] - let addButton = playersNavigationBar.buttons["Add"] - addButton.tap() - addButton.tap() - addButton.tap() - - let tablesQuery = app.tables - XCTAssertEqual(tablesQuery.cells.count, 3, "Expected 3 items") - tablesQuery.cells.element(boundBy: 1).tap() - app.navigationBars["Character Sheet"].buttons["Players"].tap() - - // TODO: more checks - } - -} diff --git a/CharacterGenerator/CharacterGeneratorUITests/Info.plist b/CharacterGenerator/CharacterGeneratorUITests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/CharacterGenerator/CharacterGeneratorUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/RolePlayingCore.xcworkspace/contents.xcworkspacedata b/Examples/CharacterGenerator/CharacterGenerator.xcworkspace/contents.xcworkspacedata similarity index 66% rename from RolePlayingCore.xcworkspace/contents.xcworkspacedata rename to Examples/CharacterGenerator/CharacterGenerator.xcworkspace/contents.xcworkspacedata index 90c313c..e7f6926 100644 --- a/RolePlayingCore.xcworkspace/contents.xcworkspacedata +++ b/Examples/CharacterGenerator/CharacterGenerator.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d19311e --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj @@ -0,0 +1,643 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + B6E1ADAB2EB3BC5B00000512 /* RolePlayingCore in Frameworks */ = {isa = PBXBuildFile; productRef = B6E1ADAA2EB3BC5B00000512 /* RolePlayingCore */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B6E1AD8D2EB3BC2D00000512 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B6E1AD752EB3BC2C00000512 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B6E1AD7C2EB3BC2C00000512; + remoteInfo = CharacterGenerator; + }; + B6E1AD972EB3BC2D00000512 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B6E1AD752EB3BC2C00000512 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B6E1AD7C2EB3BC2C00000512; + remoteInfo = CharacterGenerator; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + B6E1AD7D2EB3BC2C00000512 /* CharacterGenerator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CharacterGenerator.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B6E1AD8C2EB3BC2D00000512 /* CharacterGeneratorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B6E1AD962EB3BC2D00000512 /* CharacterGeneratorUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CharacterGeneratorUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B6E1AD7F2EB3BC2C00000512 /* CharacterGenerator */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CharacterGenerator; + sourceTree = ""; + }; + B6E1AD8F2EB3BC2D00000512 /* CharacterGeneratorTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CharacterGeneratorTests; + sourceTree = ""; + }; + B6E1AD992EB3BC2D00000512 /* CharacterGeneratorUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = CharacterGeneratorUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + B6E1AD7A2EB3BC2C00000512 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B6E1ADAB2EB3BC5B00000512 /* RolePlayingCore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD892EB3BC2D00000512 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD932EB3BC2D00000512 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B6E1AD742EB3BC2C00000512 = { + isa = PBXGroup; + children = ( + B6E1AD7F2EB3BC2C00000512 /* CharacterGenerator */, + B6E1AD8F2EB3BC2D00000512 /* CharacterGeneratorTests */, + B6E1AD992EB3BC2D00000512 /* CharacterGeneratorUITests */, + B6E1AD7E2EB3BC2C00000512 /* Products */, + ); + sourceTree = ""; + }; + B6E1AD7E2EB3BC2C00000512 /* Products */ = { + isa = PBXGroup; + children = ( + B6E1AD7D2EB3BC2C00000512 /* CharacterGenerator.app */, + B6E1AD8C2EB3BC2D00000512 /* CharacterGeneratorTests.xctest */, + B6E1AD962EB3BC2D00000512 /* CharacterGeneratorUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B6E1AD7C2EB3BC2C00000512 /* CharacterGenerator */ = { + isa = PBXNativeTarget; + buildConfigurationList = B6E1ADA02EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGenerator" */; + buildPhases = ( + B6E1AD792EB3BC2C00000512 /* Sources */, + B6E1AD7A2EB3BC2C00000512 /* Frameworks */, + B6E1AD7B2EB3BC2C00000512 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + B6E1AD7F2EB3BC2C00000512 /* CharacterGenerator */, + ); + name = CharacterGenerator; + packageProductDependencies = ( + B6E1ADAA2EB3BC5B00000512 /* RolePlayingCore */, + ); + productName = CharacterGenerator; + productReference = B6E1AD7D2EB3BC2C00000512 /* CharacterGenerator.app */; + productType = "com.apple.product-type.application"; + }; + B6E1AD8B2EB3BC2D00000512 /* CharacterGeneratorTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B6E1ADA32EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGeneratorTests" */; + buildPhases = ( + B6E1AD882EB3BC2D00000512 /* Sources */, + B6E1AD892EB3BC2D00000512 /* Frameworks */, + B6E1AD8A2EB3BC2D00000512 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B6E1AD8E2EB3BC2D00000512 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + B6E1AD8F2EB3BC2D00000512 /* CharacterGeneratorTests */, + ); + name = CharacterGeneratorTests; + packageProductDependencies = ( + ); + productName = CharacterGeneratorTests; + productReference = B6E1AD8C2EB3BC2D00000512 /* CharacterGeneratorTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + B6E1AD952EB3BC2D00000512 /* CharacterGeneratorUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B6E1ADA62EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGeneratorUITests" */; + buildPhases = ( + B6E1AD922EB3BC2D00000512 /* Sources */, + B6E1AD932EB3BC2D00000512 /* Frameworks */, + B6E1AD942EB3BC2D00000512 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B6E1AD982EB3BC2D00000512 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + B6E1AD992EB3BC2D00000512 /* CharacterGeneratorUITests */, + ); + name = CharacterGeneratorUITests; + packageProductDependencies = ( + ); + productName = CharacterGeneratorUITests; + productReference = B6E1AD962EB3BC2D00000512 /* CharacterGeneratorUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B6E1AD752EB3BC2C00000512 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2600; + LastUpgradeCheck = 2600; + TargetAttributes = { + B6E1AD7C2EB3BC2C00000512 = { + CreatedOnToolsVersion = 26.0.1; + }; + B6E1AD8B2EB3BC2D00000512 = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = B6E1AD7C2EB3BC2C00000512; + }; + B6E1AD952EB3BC2D00000512 = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = B6E1AD7C2EB3BC2C00000512; + }; + }; + }; + buildConfigurationList = B6E1AD782EB3BC2C00000512 /* Build configuration list for PBXProject "CharacterGenerator" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B6E1AD742EB3BC2C00000512; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + B6E1ADA92EB3BC5B00000512 /* XCLocalSwiftPackageReference "../../../../RolePlayingCore" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = B6E1AD7E2EB3BC2C00000512 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B6E1AD7C2EB3BC2C00000512 /* CharacterGenerator */, + B6E1AD8B2EB3BC2D00000512 /* CharacterGeneratorTests */, + B6E1AD952EB3BC2D00000512 /* CharacterGeneratorUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B6E1AD7B2EB3BC2C00000512 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD8A2EB3BC2D00000512 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD942EB3BC2D00000512 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B6E1AD792EB3BC2C00000512 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD882EB3BC2D00000512 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B6E1AD922EB3BC2D00000512 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B6E1AD8E2EB3BC2D00000512 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B6E1AD7C2EB3BC2C00000512 /* CharacterGenerator */; + targetProxy = B6E1AD8D2EB3BC2D00000512 /* PBXContainerItemProxy */; + }; + B6E1AD982EB3BC2D00000512 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B6E1AD7C2EB3BC2C00000512 /* CharacterGenerator */; + targetProxy = B6E1AD972EB3BC2D00000512 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + B6E1AD9E2EB3BC2D00000512 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = VHX6TEH729; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B6E1AD9F2EB3BC2D00000512 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = VHX6TEH729; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + B6E1ADA12EB3BC2D00000512 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = VHX6TEH729; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + B6E1ADA22EB3BC2D00000512 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = VHX6TEH729; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; + "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; + "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; + B6E1ADA42EB3BC2D00000512 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = VHX6TEH729; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CharacterGenerator.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CharacterGenerator"; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + B6E1ADA52EB3BC2D00000512 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = VHX6TEH729; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CharacterGenerator.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CharacterGenerator"; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; + B6E1ADA72EB3BC2D00000512 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = VHX6TEH729; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = CharacterGenerator; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Debug; + }; + B6E1ADA82EB3BC2D00000512 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = VHX6TEH729; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGeneratorUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + TEST_TARGET_NAME = CharacterGenerator; + XROS_DEPLOYMENT_TARGET = 26.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B6E1AD782EB3BC2C00000512 /* Build configuration list for PBXProject "CharacterGenerator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6E1AD9E2EB3BC2D00000512 /* Debug */, + B6E1AD9F2EB3BC2D00000512 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B6E1ADA02EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGenerator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6E1ADA12EB3BC2D00000512 /* Debug */, + B6E1ADA22EB3BC2D00000512 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B6E1ADA32EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGeneratorTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6E1ADA42EB3BC2D00000512 /* Debug */, + B6E1ADA52EB3BC2D00000512 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B6E1ADA62EB3BC2D00000512 /* Build configuration list for PBXNativeTarget "CharacterGeneratorUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B6E1ADA72EB3BC2D00000512 /* Debug */, + B6E1ADA82EB3BC2D00000512 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + B6E1ADA92EB3BC5B00000512 /* XCLocalSwiftPackageReference "../../../../RolePlayingCore" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../../../RolePlayingCore; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + B6E1ADAA2EB3BC5B00000512 /* RolePlayingCore */ = { + isa = XCSwiftPackageProductDependency; + productName = RolePlayingCore; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = B6E1AD752EB3BC2C00000512 /* Project object */; +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..ffdfe15 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,85 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json similarity index 100% rename from CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Assets.xcassets/Contents.json diff --git a/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift similarity index 99% rename from CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift index 8a35e9e..947e9f2 100644 --- a/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/CharacterGeneratorApp.swift @@ -8,6 +8,7 @@ import SwiftUI import RolePlayingCore +import Combine @main struct CharacterGeneratorApp: App { diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json similarity index 100% rename from CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Backgrounds.json diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Classes.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Classes.json similarity index 100% rename from CharacterGenerator/CharacterGenerator/Configuration/Classes.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Classes.json diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json similarity index 86% rename from CharacterGenerator/CharacterGenerator/Configuration/Configuration.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json index bffe9a7..1252ee6 100644 --- a/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json @@ -1,5 +1,6 @@ { "currencies": ["Currencies"], + "skills": ["Skills"], "backgrounds": ["Backgrounds"], "classes": ["Classes"], "species": ["Species"], diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Currencies.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Currencies.json similarity index 100% rename from CharacterGenerator/CharacterGenerator/Configuration/Currencies.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Currencies.json diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Skills.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Skills.json new file mode 100644 index 0000000..a1441e2 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Skills.json @@ -0,0 +1,76 @@ +{ + "skills": [ + { + "name": "Acrobatics", + "ability": "Dexterity" + }, + { + "name": "Animal Handling", + "ability": "Wisdom" + }, + { + "name": "Arcana", + "ability": "Intelligence" + }, + { + "name": "Athletics", + "ability": "Strength" + }, + { + "name": "Deception", + "ability": "Charisma" + }, + { + "name": "History", + "ability": "Intelligence" + }, + { + "name": "Insight", + "ability": "Wisdom" + }, + { + "name": "Intimidation", + "ability": "Charisma" + }, + { + "name": "Investigation", + "ability": "Intelligence" + }, + { + "name": "Medicine", + "ability": "Wisdom" + }, + { + "name": "Nature", + "ability": "Intelligence" + }, + { + "name": "Perception", + "ability": "Wisdom" + }, + { + "name": "Performance", + "ability": "Charisma" + }, + { + "name": "Persuasion", + "ability": "Charisma" + }, + { + "name": "Religion", + "ability": "Intelligence" + }, + { + "name": "Sleight of Hand", + "ability": "Dexterity" + }, + { + "name": "Stealth", + "ability": "Dexterity" + }, + { + "name": "Survival", + "ability": "Wisdom" + } + ] +} diff --git a/CharacterGenerator/CharacterGenerator/Configuration/Species.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json similarity index 100% rename from CharacterGenerator/CharacterGenerator/Configuration/Species.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json diff --git a/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json similarity index 100% rename from CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/SpeciesNames.json diff --git a/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift similarity index 98% rename from CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift index 7a37a05..349e18e 100644 --- a/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift @@ -84,7 +84,7 @@ class CharacterSheet { } var abilities: AbilityScores { player.abilities } var skills: String { - player.skills.map(\.name).joined(separator: ", ") + player.skillProficiencies.joined(separator: ", ") } var initiative: String { player.initiativeModifier.displayModifier } var armorClass: String { "\(player.armorClass)" } diff --git a/CharacterGenerator/CharacterGenerator/Player/ExperiencePoints.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/ExperiencePoints.swift similarity index 100% rename from CharacterGenerator/CharacterGenerator/Player/ExperiencePoints.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/ExperiencePoints.swift diff --git a/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift similarity index 100% rename from CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift diff --git a/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift similarity index 100% rename from CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift rename to Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift new file mode 100644 index 0000000..b41ef9a --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorTests/CharacterGeneratorTests.swift @@ -0,0 +1,16 @@ +// +// CharacterGeneratorTests.swift +// CharacterGeneratorTests +// +// Created by Brian Arnold on 10/30/25. +// + +import Testing + +struct CharacterGeneratorTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift new file mode 100644 index 0000000..26d28d9 --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITests.swift @@ -0,0 +1,41 @@ +// +// CharacterGeneratorUITests.swift +// CharacterGeneratorUITests +// +// Created by Brian Arnold on 10/30/25. +// + +import XCTest + +final class CharacterGeneratorUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITestsLaunchTests.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITestsLaunchTests.swift new file mode 100644 index 0000000..40c580c --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGeneratorUITests/CharacterGeneratorUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// CharacterGeneratorUITestsLaunchTests.swift +// CharacterGeneratorUITests +// +// Created by Brian Arnold on 10/30/25. +// + +import XCTest + +final class CharacterGeneratorUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..0269648 --- /dev/null +++ b/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "RolePlayingCore", + platforms: [ + .iOS(.v17), + .macOS(.v14) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "RolePlayingCore", + targets: ["RolePlayingCore"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "RolePlayingCore", + path: "Sources/RolePlayingCore"), + .testTarget( + name: "RolePlayingCoreTests", + dependencies: ["RolePlayingCore"], + path: "Tests/RolePlayingCoreTests", + resources: [ + .process("TestResources") + ]), + ] +) diff --git a/README.md b/README.md index 2501330..a1a81a0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The short-term goal for this project is to provide core logic for implementing a ## Requirements -Xcode 26 or Swift 5 are required. +Xcode 26 or Swift 6 are required. ## Organization diff --git a/RolePlayingCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/RolePlayingCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/RolePlayingCore.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj b/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj deleted file mode 100644 index 6f190d0..0000000 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/project.pbxproj +++ /dev/null @@ -1,806 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - B62055F81E19DD23002494AB /* RolePlayingCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B62055EE1E19DD23002494AB /* RolePlayingCore.framework */; }; - B62055FF1E19DD23002494AB /* RolePlayingCore.h in Headers */ = {isa = PBXBuildFile; fileRef = B62055F11E19DD23002494AB /* RolePlayingCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B620560A1E19DDC0002494AB /* DiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62056091E19DDC0002494AB /* DiceTests.swift */; }; - B620560E1E19DDD0002494AB /* Dice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B620560B1E19DDD0002494AB /* Dice.swift */; }; - B620560F1E19DDD0002494AB /* DiceParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B620560C1E19DDD0002494AB /* DiceParser.swift */; }; - B621A3981F0C052D00E55236 /* NameGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3971F0C052D00E55236 /* NameGeneratorTests.swift */; }; - B621A3991F0C06A700E55236 /* NameGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B621A3961F0C020000E55236 /* NameGenerator.swift */; }; - B626FA592EAE76AE00359F01 /* Skill.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626FA582EAE76AA00359F01 /* Skill.swift */; }; - B626FA5B2EAE803000359F01 /* BackgroundTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626FA5A2EAE802D00359F01 /* BackgroundTraits.swift */; }; - B626FA5D2EAE81C900359F01 /* Backgrounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626FA5C2EAE81C600359F01 /* Backgrounds.swift */; }; - B626FA612EAE919900359F01 /* TestBackgrounds.json in Resources */ = {isa = PBXBuildFile; fileRef = B626FA602EAE919200359F01 /* TestBackgrounds.json */; }; - B626FA642EAF9FCF00359F01 /* BackgroundsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B626FA622EAF9F9200359F01 /* BackgroundsTests.swift */; }; - B62D89C41F09A3870095D587 /* DiceParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62D89C31F09A3870095D587 /* DiceParserTests.swift */; }; - B64C369621756BC300C4F6BE /* DiceRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C369521756BC300C4F6BE /* DiceRoll.swift */; }; - B6688B512EACF5B7000A83DD /* Initiative.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6688B502EACF5AE000A83DD /* Initiative.swift */; }; - B66AF8FC1EAE87A800C15F8E /* TestClasses.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F41EAE87A800C15F8E /* TestClasses.json */; }; - B66AF8FD1EAE87A800C15F8E /* TestConfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F51EAE87A800C15F8E /* TestConfiguration.json */; }; - B66AF8FE1EAE87A800C15F8E /* TestCurrencies.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F61EAE87A800C15F8E /* TestCurrencies.json */; }; - B66AF8FF1EAE87A800C15F8E /* TestMoreClasses.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F71EAE87A800C15F8E /* TestMoreClasses.json */; }; - B66AF9001EAE87A800C15F8E /* TestMoreSpecies.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F81EAE87A800C15F8E /* TestMoreSpecies.json */; }; - B66AF9011EAE87A800C15F8E /* TestNames.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8F91EAE87A800C15F8E /* TestNames.json */; }; - B66AF9021EAE87A800C15F8E /* TestPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8FA1EAE87A800C15F8E /* TestPlayers.json */; }; - B66AF9031EAE87A800C15F8E /* TestSpecies.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF8FB1EAE87A800C15F8E /* TestSpecies.json */; }; - B66AF9051EAE88C300C15F8E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66AF9041EAE88C300C15F8E /* Configuration.swift */; }; - B66AF9071EAE88FF00C15F8E /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66AF9061EAE88FF00C15F8E /* ConfigurationTests.swift */; }; - B66AF9091EAE8B8100C15F8E /* MissingClassPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF9081EAE8B8000C15F8E /* MissingClassPlayers.json */; }; - B66AF90B1EAE8BF100C15F8E /* MissingSpeciesPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B66AF90A1EAE8BF100C15F8E /* MissingSpeciesPlayers.json */; }; - B67450E41F118A8B0061FD6F /* SpeciesNamesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67450E31F118A8B0061FD6F /* SpeciesNamesTests.swift */; }; - B681B7161EAAC8E2001DE78B /* SimpleDice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B7151EAAC8E2001DE78B /* SimpleDice.swift */; }; - B681B7181EAAC8EE001DE78B /* DiceModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B7171EAAC8EE001DE78B /* DiceModifier.swift */; }; - B681B71A1EAAC8FB001DE78B /* CompoundDice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B7191EAAC8FB001DE78B /* CompoundDice.swift */; }; - B681B71C1EAAC904001DE78B /* DroppingDice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B71B1EAAC904001DE78B /* DroppingDice.swift */; }; - B681B71E1EAACA9B001DE78B /* Die.swift in Sources */ = {isa = PBXBuildFile; fileRef = B681B71D1EAACA9B001DE78B /* Die.swift */; }; - B698515E1F1171EC006A537A /* SpeciesNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = B698515D1F1171EC006A537A /* SpeciesNames.swift */; }; - B69851621F11850A006A537A /* TestSpeciesNames.json in Resources */ = {isa = PBXBuildFile; fileRef = B69851611F11850A006A537A /* TestSpeciesNames.json */; }; - B69F84691E58B8F700A4D2B0 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69F84681E58B8F700A4D2B0 /* Player.swift */; }; - B69F846B1E58D33900A4D2B0 /* PlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69F846A1E58D33900A4D2B0 /* PlayerTests.swift */; }; - B69F846D1E58D66900A4D2B0 /* Players.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69F846C1E58D66900A4D2B0 /* Players.swift */; }; - B69F846F1E59155A00A4D2B0 /* PlayersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69F846E1E59155A00A4D2B0 /* PlayersTests.swift */; }; - B69F84731E591DE400A4D2B0 /* InvalidClassPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B69F84721E591DE400A4D2B0 /* InvalidClassPlayers.json */; }; - B69F84751E591E9800A4D2B0 /* InvalidSpeciesPlayers.json in Resources */ = {isa = PBXBuildFile; fileRef = B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */; }; - B6A1AE591F0E4C59008ADF08 /* InvalidConfiguration.json in Resources */ = {isa = PBXBuildFile; fileRef = B6A1AE581F0E4C59008ADF08 /* InvalidConfiguration.json */; }; - B6A29EB51EFE9B9F00DAB40C /* Currencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A29EB41EFE9B9F00DAB40C /* Currencies.swift */; }; - B6C3076E2EAFBEC10066D9F0 /* CreatureSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */; }; - B6CF53901E51DA1300CADD9F /* ClassTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */; }; - B6CF53921E51DEDD00CADD9F /* ClassTraitsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */; }; - B6CF53CE1E54DF4500CADD9F /* JSONFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53CD1E54DF4500CADD9F /* JSONFile.swift */; }; - B6CF53D01E54E2E200CADD9F /* Classes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53CF1E54E2E200CADD9F /* Classes.swift */; }; - B6CF53D21E54E2EF00CADD9F /* Species.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53D11E54E2EF00CADD9F /* Species.swift */; }; - B6CF53D91E56442800CADD9F /* SpeciesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53D81E56442800CADD9F /* SpeciesTests.swift */; }; - B6CF53DB1E56443A00CADD9F /* ClassesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53DA1E56443A00CADD9F /* ClassesTests.swift */; }; - B6CF53E51E57429D00CADD9F /* JSONFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CF53E41E57429D00CADD9F /* JSONFileTests.swift */; }; - B6CF53E71E5742C700CADD9F /* JSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53E61E5742C700CADD9F /* JSONFile.json */; }; - B6CF53E91E57460F00CADD9F /* InvalidJSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53E81E57460F00CADD9F /* InvalidJSONFile.json */; }; - B6CF53EB1E574ED400CADD9F /* HalfBakedJSONFile.json in Resources */ = {isa = PBXBuildFile; fileRef = B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */; }; - B6D226F82EB25ABF00939968 /* TestBundleClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D226F72EB25AB900939968 /* TestBundleClass.swift */; }; - B6D2EB1F26D7B7E900F99B35 /* RandomIndexGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */; }; - B6E18DF62EB26B9B000B3C90 /* DiceEncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E18DF52EB26B96000B3C90 /* DiceEncodingTests.swift */; }; - B6F070361E4F991D00F66918 /* AlignmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070351E4F991D00F66918 /* AlignmentTests.swift */; }; - B6F070381E4FBD6500F66918 /* SpeciesTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */; }; - B6F0703A1E4FC03700F66918 /* Money.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F070391E4FC03700F66918 /* Money.swift */; }; - B6F0703C1E4FC18500F66918 /* SpeciesTraitsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F0703B1E4FC18500F66918 /* SpeciesTraitsTests.swift */; }; - B6F4AA481F12C9FA000C72D2 /* CharacterGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F4AA471F12C9FA000C72D2 /* CharacterGenerator.swift */; }; - B6F4AA4A1F12CD2A000C72D2 /* CharacterGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F4AA491F12CD2A000C72D2 /* CharacterGeneratorTests.swift */; }; - B6F4AA4C1F12CE17000C72D2 /* TestCharacterGenerator.json in Resources */ = {isa = PBXBuildFile; fileRef = B6F4AA4B1F12CE17000C72D2 /* TestCharacterGenerator.json */; }; - B6FA6CB01E47ACA1004D91B1 /* UnitCurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CAF1E47ACA1004D91B1 /* UnitCurrency.swift */; }; - B6FA6CB41E47ACCC004D91B1 /* ServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CB31E47ACCC004D91B1 /* ServiceError.swift */; }; - B6FA6CB61E47B080004D91B1 /* CurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CB51E47B080004D91B1 /* CurrencyTests.swift */; }; - B6FA6CB91E47B4B1004D91B1 /* ServiceErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CB81E47B4B1004D91B1 /* ServiceErrorTests.swift */; }; - B6FA6CBB1E47B7F6004D91B1 /* Height.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CBA1E47B7F6004D91B1 /* Height.swift */; }; - B6FA6CBD1E47B803004D91B1 /* Weight.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CBC1E47B803004D91B1 /* Weight.swift */; }; - B6FA6CBF1E47C2F9004D91B1 /* HeightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CBE1E47C2F9004D91B1 /* HeightTests.swift */; }; - B6FA6CC11E47C306004D91B1 /* WeightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CC01E47C306004D91B1 /* WeightTests.swift */; }; - B6FA6CC51E4A96D1004D91B1 /* Ability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CC41E4A96D1004D91B1 /* Ability.swift */; }; - B6FA6CC91E4BF5F9004D91B1 /* AbilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */; }; - B6FA6CCB1E4E7AEA004D91B1 /* Alignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA6CCA1E4E7AEA004D91B1 /* Alignment.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - B62055F91E19DD23002494AB /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = B62055E51E19DD23002494AB /* Project object */; - proxyType = 1; - remoteGlobalIDString = B62055ED1E19DD23002494AB; - remoteInfo = RolePlayingCore; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - B62055EE1E19DD23002494AB /* RolePlayingCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RolePlayingCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B62055F11E19DD23002494AB /* RolePlayingCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RolePlayingCore.h; sourceTree = ""; }; - B62055F21E19DD23002494AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B62055F71E19DD23002494AB /* RolePlayingCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RolePlayingCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - B62055FE1E19DD23002494AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B62056091E19DDC0002494AB /* DiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceTests.swift; sourceTree = ""; }; - B620560B1E19DDD0002494AB /* Dice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dice.swift; sourceTree = ""; }; - B620560C1E19DDD0002494AB /* DiceParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceParser.swift; sourceTree = ""; }; - B621A3961F0C020000E55236 /* NameGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGenerator.swift; sourceTree = ""; }; - B621A3971F0C052D00E55236 /* NameGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGeneratorTests.swift; sourceTree = ""; }; - B626FA582EAE76AA00359F01 /* Skill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Skill.swift; sourceTree = ""; }; - B626FA5A2EAE802D00359F01 /* BackgroundTraits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTraits.swift; sourceTree = ""; }; - B626FA5C2EAE81C600359F01 /* Backgrounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backgrounds.swift; sourceTree = ""; }; - B626FA602EAE919200359F01 /* TestBackgrounds.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TestBackgrounds.json; sourceTree = ""; }; - B626FA622EAF9F9200359F01 /* BackgroundsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundsTests.swift; sourceTree = ""; }; - B62D89C31F09A3870095D587 /* DiceParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiceParserTests.swift; sourceTree = ""; }; - B64C369521756BC300C4F6BE /* DiceRoll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceRoll.swift; sourceTree = ""; }; - B6688B502EACF5AE000A83DD /* Initiative.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initiative.swift; sourceTree = ""; }; - B66AF8F41EAE87A800C15F8E /* TestClasses.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestClasses.json; sourceTree = ""; }; - B66AF8F51EAE87A800C15F8E /* TestConfiguration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestConfiguration.json; sourceTree = ""; }; - B66AF8F61EAE87A800C15F8E /* TestCurrencies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestCurrencies.json; sourceTree = ""; }; - B66AF8F71EAE87A800C15F8E /* TestMoreClasses.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestMoreClasses.json; sourceTree = ""; }; - B66AF8F81EAE87A800C15F8E /* TestMoreSpecies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestMoreSpecies.json; sourceTree = ""; }; - B66AF8F91EAE87A800C15F8E /* TestNames.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestNames.json; sourceTree = ""; }; - B66AF8FA1EAE87A800C15F8E /* TestPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestPlayers.json; sourceTree = ""; }; - B66AF8FB1EAE87A800C15F8E /* TestSpecies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestSpecies.json; sourceTree = ""; }; - B66AF9041EAE88C300C15F8E /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - B66AF9061EAE88FF00C15F8E /* ConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigurationTests.swift; sourceTree = ""; }; - B66AF9081EAE8B8000C15F8E /* MissingClassPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = MissingClassPlayers.json; sourceTree = ""; }; - B66AF90A1EAE8BF100C15F8E /* MissingSpeciesPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = MissingSpeciesPlayers.json; sourceTree = ""; }; - B67450E31F118A8B0061FD6F /* SpeciesNamesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeciesNamesTests.swift; sourceTree = ""; }; - B681B7151EAAC8E2001DE78B /* SimpleDice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleDice.swift; sourceTree = ""; }; - B681B7171EAAC8EE001DE78B /* DiceModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiceModifier.swift; sourceTree = ""; }; - B681B7191EAAC8FB001DE78B /* CompoundDice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundDice.swift; sourceTree = ""; }; - B681B71B1EAAC904001DE78B /* DroppingDice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DroppingDice.swift; sourceTree = ""; }; - B681B71D1EAACA9B001DE78B /* Die.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Die.swift; sourceTree = ""; }; - B698515D1F1171EC006A537A /* SpeciesNames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeciesNames.swift; sourceTree = ""; }; - B69851611F11850A006A537A /* TestSpeciesNames.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TestSpeciesNames.json; sourceTree = ""; }; - B69F84681E58B8F700A4D2B0 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; - B69F846A1E58D33900A4D2B0 /* PlayerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTests.swift; sourceTree = ""; }; - B69F846C1E58D66900A4D2B0 /* Players.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Players.swift; sourceTree = ""; }; - B69F846E1E59155A00A4D2B0 /* PlayersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayersTests.swift; sourceTree = ""; }; - B69F84721E591DE400A4D2B0 /* InvalidClassPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidClassPlayers.json; sourceTree = ""; }; - B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidSpeciesPlayers.json; sourceTree = ""; }; - B6A1AE581F0E4C59008ADF08 /* InvalidConfiguration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = InvalidConfiguration.json; sourceTree = ""; }; - B6A29EB41EFE9B9F00DAB40C /* Currencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Currencies.swift; sourceTree = ""; }; - B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatureSize.swift; sourceTree = ""; }; - B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassTraits.swift; sourceTree = ""; }; - B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassTraitsTests.swift; sourceTree = ""; }; - B6CF53CD1E54DF4500CADD9F /* JSONFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFile.swift; sourceTree = ""; }; - B6CF53CF1E54E2E200CADD9F /* Classes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Classes.swift; sourceTree = ""; }; - B6CF53D11E54E2EF00CADD9F /* Species.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Species.swift; sourceTree = ""; }; - B6CF53D81E56442800CADD9F /* SpeciesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeciesTests.swift; sourceTree = ""; }; - B6CF53DA1E56443A00CADD9F /* ClassesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassesTests.swift; sourceTree = ""; }; - B6CF53E41E57429D00CADD9F /* JSONFileTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFileTests.swift; sourceTree = ""; }; - B6CF53E61E5742C700CADD9F /* JSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = JSONFile.json; sourceTree = ""; }; - B6CF53E81E57460F00CADD9F /* InvalidJSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InvalidJSONFile.json; sourceTree = ""; }; - B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HalfBakedJSONFile.json; sourceTree = ""; }; - B6D226F72EB25AB900939968 /* TestBundleClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestBundleClass.swift; sourceTree = ""; }; - B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomIndexGenerator.swift; sourceTree = ""; }; - B6E18DF52EB26B96000B3C90 /* DiceEncodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiceEncodingTests.swift; sourceTree = ""; }; - B6F070351E4F991D00F66918 /* AlignmentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlignmentTests.swift; sourceTree = ""; }; - B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeciesTraits.swift; sourceTree = ""; }; - B6F070391E4FC03700F66918 /* Money.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Money.swift; sourceTree = ""; }; - B6F0703B1E4FC18500F66918 /* SpeciesTraitsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeciesTraitsTests.swift; sourceTree = ""; }; - B6F4AA471F12C9FA000C72D2 /* CharacterGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CharacterGenerator.swift; sourceTree = ""; }; - B6F4AA491F12CD2A000C72D2 /* CharacterGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterGeneratorTests.swift; sourceTree = ""; }; - B6F4AA4B1F12CE17000C72D2 /* TestCharacterGenerator.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TestCharacterGenerator.json; sourceTree = ""; }; - B6FA6CAF1E47ACA1004D91B1 /* UnitCurrency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitCurrency.swift; sourceTree = ""; }; - B6FA6CB31E47ACCC004D91B1 /* ServiceError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceError.swift; sourceTree = ""; }; - B6FA6CB51E47B080004D91B1 /* CurrencyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyTests.swift; sourceTree = ""; }; - B6FA6CB81E47B4B1004D91B1 /* ServiceErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceErrorTests.swift; sourceTree = ""; }; - B6FA6CBA1E47B7F6004D91B1 /* Height.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Height.swift; sourceTree = ""; }; - B6FA6CBC1E47B803004D91B1 /* Weight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weight.swift; sourceTree = ""; }; - B6FA6CBE1E47C2F9004D91B1 /* HeightTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeightTests.swift; sourceTree = ""; }; - B6FA6CC01E47C306004D91B1 /* WeightTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeightTests.swift; sourceTree = ""; }; - B6FA6CC41E4A96D1004D91B1 /* Ability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ability.swift; sourceTree = ""; }; - B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AbilityTests.swift; sourceTree = ""; }; - B6FA6CCA1E4E7AEA004D91B1 /* Alignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Alignment.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - B62055EA1E19DD23002494AB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B62055F41E19DD23002494AB /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B62055F81E19DD23002494AB /* RolePlayingCore.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - B62055E41E19DD23002494AB = { - isa = PBXGroup; - children = ( - B62055F01E19DD23002494AB /* RolePlayingCore */, - B62055FB1E19DD23002494AB /* RolePlayingCoreTests */, - B62055EF1E19DD23002494AB /* Products */, - ); - sourceTree = ""; - }; - B62055EF1E19DD23002494AB /* Products */ = { - isa = PBXGroup; - children = ( - B62055EE1E19DD23002494AB /* RolePlayingCore.framework */, - B62055F71E19DD23002494AB /* RolePlayingCoreTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - B62055F01E19DD23002494AB /* RolePlayingCore */ = { - isa = PBXGroup; - children = ( - B6F4AA461F12C9E5000C72D2 /* Configuration */, - B6FA6CAD1E47AC8D004D91B1 /* Common */, - B6FA6CAE1E47AC8D004D91B1 /* Currency */, - B62056081E19DD5D002494AB /* Dice */, - B6FA6CC31E4A96C2004D91B1 /* Player */, - B62055F11E19DD23002494AB /* RolePlayingCore.h */, - B62055F21E19DD23002494AB /* Info.plist */, - ); - path = RolePlayingCore; - sourceTree = ""; - }; - B62055FB1E19DD23002494AB /* RolePlayingCoreTests */ = { - isa = PBXGroup; - children = ( - B6D226F72EB25AB900939968 /* TestBundleClass.swift */, - B6FA6CC81E4BF5F9004D91B1 /* AbilityTests.swift */, - B6F070351E4F991D00F66918 /* AlignmentTests.swift */, - B626FA622EAF9F9200359F01 /* BackgroundsTests.swift */, - B69F846A1E58D33900A4D2B0 /* PlayerTests.swift */, - B69F846E1E59155A00A4D2B0 /* PlayersTests.swift */, - B6FA6CB51E47B080004D91B1 /* CurrencyTests.swift */, - B62056091E19DDC0002494AB /* DiceTests.swift */, - B62D89C31F09A3870095D587 /* DiceParserTests.swift */, - B6E18DF52EB26B96000B3C90 /* DiceEncodingTests.swift */, - B6FA6CBE1E47C2F9004D91B1 /* HeightTests.swift */, - B6FA6CC01E47C306004D91B1 /* WeightTests.swift */, - B6CF53911E51DEDD00CADD9F /* ClassTraitsTests.swift */, - B6CF53DA1E56443A00CADD9F /* ClassesTests.swift */, - B6F0703B1E4FC18500F66918 /* SpeciesTraitsTests.swift */, - B6CF53D81E56442800CADD9F /* SpeciesTests.swift */, - B66AF9061EAE88FF00C15F8E /* ConfigurationTests.swift */, - B6CF53E41E57429D00CADD9F /* JSONFileTests.swift */, - B6FA6CB81E47B4B1004D91B1 /* ServiceErrorTests.swift */, - B67450E31F118A8B0061FD6F /* SpeciesNamesTests.swift */, - B621A3971F0C052D00E55236 /* NameGeneratorTests.swift */, - B6F4AA491F12CD2A000C72D2 /* CharacterGeneratorTests.swift */, - B6CF53E61E5742C700CADD9F /* JSONFile.json */, - B6CF53E81E57460F00CADD9F /* InvalidJSONFile.json */, - B6CF53EA1E574ED400CADD9F /* HalfBakedJSONFile.json */, - B66AF9081EAE8B8000C15F8E /* MissingClassPlayers.json */, - B66AF90A1EAE8BF100C15F8E /* MissingSpeciesPlayers.json */, - B69F84721E591DE400A4D2B0 /* InvalidClassPlayers.json */, - B69F84741E591E9800A4D2B0 /* InvalidSpeciesPlayers.json */, - B66AF8F41EAE87A800C15F8E /* TestClasses.json */, - B626FA602EAE919200359F01 /* TestBackgrounds.json */, - B66AF8F71EAE87A800C15F8E /* TestMoreClasses.json */, - B66AF8F51EAE87A800C15F8E /* TestConfiguration.json */, - B6F4AA4B1F12CE17000C72D2 /* TestCharacterGenerator.json */, - B66AF8F61EAE87A800C15F8E /* TestCurrencies.json */, - B66AF8F91EAE87A800C15F8E /* TestNames.json */, - B69851611F11850A006A537A /* TestSpeciesNames.json */, - B66AF8FA1EAE87A800C15F8E /* TestPlayers.json */, - B66AF8FB1EAE87A800C15F8E /* TestSpecies.json */, - B66AF8F81EAE87A800C15F8E /* TestMoreSpecies.json */, - B6A1AE581F0E4C59008ADF08 /* InvalidConfiguration.json */, - B62055FE1E19DD23002494AB /* Info.plist */, - ); - path = RolePlayingCoreTests; - sourceTree = ""; - }; - B62056081E19DD5D002494AB /* Dice */ = { - isa = PBXGroup; - children = ( - B681B71D1EAACA9B001DE78B /* Die.swift */, - B620560B1E19DDD0002494AB /* Dice.swift */, - B64C369521756BC300C4F6BE /* DiceRoll.swift */, - B681B7151EAAC8E2001DE78B /* SimpleDice.swift */, - B681B7171EAAC8EE001DE78B /* DiceModifier.swift */, - B681B7191EAAC8FB001DE78B /* CompoundDice.swift */, - B681B71B1EAAC904001DE78B /* DroppingDice.swift */, - B620560C1E19DDD0002494AB /* DiceParser.swift */, - ); - path = Dice; - sourceTree = ""; - }; - B6F4AA461F12C9E5000C72D2 /* Configuration */ = { - isa = PBXGroup; - children = ( - B66AF9041EAE88C300C15F8E /* Configuration.swift */, - B6F4AA471F12C9FA000C72D2 /* CharacterGenerator.swift */, - ); - path = Configuration; - sourceTree = ""; - }; - B6FA6CAD1E47AC8D004D91B1 /* Common */ = { - isa = PBXGroup; - children = ( - B6FA6CB31E47ACCC004D91B1 /* ServiceError.swift */, - B6FA6CBA1E47B7F6004D91B1 /* Height.swift */, - B6FA6CBC1E47B803004D91B1 /* Weight.swift */, - B621A3961F0C020000E55236 /* NameGenerator.swift */, - B6D2EB1E26D7B7E900F99B35 /* RandomIndexGenerator.swift */, - B6CF53CD1E54DF4500CADD9F /* JSONFile.swift */, - B698515D1F1171EC006A537A /* SpeciesNames.swift */, - ); - path = Common; - sourceTree = ""; - }; - B6FA6CAE1E47AC8D004D91B1 /* Currency */ = { - isa = PBXGroup; - children = ( - B6FA6CAF1E47ACA1004D91B1 /* UnitCurrency.swift */, - B6A29EB41EFE9B9F00DAB40C /* Currencies.swift */, - B6F070391E4FC03700F66918 /* Money.swift */, - ); - path = Currency; - sourceTree = ""; - }; - B6FA6CC31E4A96C2004D91B1 /* Player */ = { - isa = PBXGroup; - children = ( - B6FA6CC41E4A96D1004D91B1 /* Ability.swift */, - B6FA6CCA1E4E7AEA004D91B1 /* Alignment.swift */, - B626FA5A2EAE802D00359F01 /* BackgroundTraits.swift */, - B626FA5C2EAE81C600359F01 /* Backgrounds.swift */, - B6CF538F1E51DA1300CADD9F /* ClassTraits.swift */, - B6CF53CF1E54E2E200CADD9F /* Classes.swift */, - B6C3076D2EAFBEBE0066D9F0 /* CreatureSize.swift */, - B6688B502EACF5AE000A83DD /* Initiative.swift */, - B69F84681E58B8F700A4D2B0 /* Player.swift */, - B69F846C1E58D66900A4D2B0 /* Players.swift */, - B626FA582EAE76AA00359F01 /* Skill.swift */, - B6F070371E4FBD6500F66918 /* SpeciesTraits.swift */, - B6CF53D11E54E2EF00CADD9F /* Species.swift */, - ); - path = Player; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - B62055EB1E19DD23002494AB /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - B62055FF1E19DD23002494AB /* RolePlayingCore.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - B62055ED1E19DD23002494AB /* RolePlayingCore */ = { - isa = PBXNativeTarget; - buildConfigurationList = B62056021E19DD23002494AB /* Build configuration list for PBXNativeTarget "RolePlayingCore" */; - buildPhases = ( - B62055E91E19DD23002494AB /* Sources */, - B62055EA1E19DD23002494AB /* Frameworks */, - B62055EB1E19DD23002494AB /* Headers */, - B62055EC1E19DD23002494AB /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = RolePlayingCore; - productName = RolePlayingCore; - productReference = B62055EE1E19DD23002494AB /* RolePlayingCore.framework */; - productType = "com.apple.product-type.framework"; - }; - B62055F61E19DD23002494AB /* RolePlayingCoreTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = B62056051E19DD23002494AB /* Build configuration list for PBXNativeTarget "RolePlayingCoreTests" */; - buildPhases = ( - B62055F31E19DD23002494AB /* Sources */, - B62055F41E19DD23002494AB /* Frameworks */, - B62055F51E19DD23002494AB /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - B62055FA1E19DD23002494AB /* PBXTargetDependency */, - ); - name = RolePlayingCoreTests; - productName = RolePlayingCoreTests; - productReference = B62055F71E19DD23002494AB /* RolePlayingCoreTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - B62055E51E19DD23002494AB /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 1620; - ORGANIZATIONNAME = "Brian Arnold"; - TargetAttributes = { - B62055ED1E19DD23002494AB = { - CreatedOnToolsVersion = 8.2.1; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - }; - B62055F61E19DD23002494AB = { - CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = J69E69SP27; - LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = B62055E81E19DD23002494AB /* Build configuration list for PBXProject "RolePlayingCore" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = B62055E41E19DD23002494AB; - productRefGroup = B62055EF1E19DD23002494AB /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - B62055ED1E19DD23002494AB /* RolePlayingCore */, - B62055F61E19DD23002494AB /* RolePlayingCoreTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - B62055EC1E19DD23002494AB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B62055F51E19DD23002494AB /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B69851621F11850A006A537A /* TestSpeciesNames.json in Resources */, - B6A1AE591F0E4C59008ADF08 /* InvalidConfiguration.json in Resources */, - B6CF53E91E57460F00CADD9F /* InvalidJSONFile.json in Resources */, - B66AF9021EAE87A800C15F8E /* TestPlayers.json in Resources */, - B66AF8FE1EAE87A800C15F8E /* TestCurrencies.json in Resources */, - B66AF90B1EAE8BF100C15F8E /* MissingSpeciesPlayers.json in Resources */, - B69F84751E591E9800A4D2B0 /* InvalidSpeciesPlayers.json in Resources */, - B66AF9011EAE87A800C15F8E /* TestNames.json in Resources */, - B66AF8FD1EAE87A800C15F8E /* TestConfiguration.json in Resources */, - B626FA612EAE919900359F01 /* TestBackgrounds.json in Resources */, - B66AF9091EAE8B8100C15F8E /* MissingClassPlayers.json in Resources */, - B69F84731E591DE400A4D2B0 /* InvalidClassPlayers.json in Resources */, - B66AF8FF1EAE87A800C15F8E /* TestMoreClasses.json in Resources */, - B66AF9031EAE87A800C15F8E /* TestSpecies.json in Resources */, - B6F4AA4C1F12CE17000C72D2 /* TestCharacterGenerator.json in Resources */, - B66AF9001EAE87A800C15F8E /* TestMoreSpecies.json in Resources */, - B6CF53EB1E574ED400CADD9F /* HalfBakedJSONFile.json in Resources */, - B66AF8FC1EAE87A800C15F8E /* TestClasses.json in Resources */, - B6CF53E71E5742C700CADD9F /* JSONFile.json in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - B62055E91E19DD23002494AB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B69F846D1E58D66900A4D2B0 /* Players.swift in Sources */, - B681B71C1EAAC904001DE78B /* DroppingDice.swift in Sources */, - B681B7161EAAC8E2001DE78B /* SimpleDice.swift in Sources */, - B6F4AA481F12C9FA000C72D2 /* CharacterGenerator.swift in Sources */, - B6FA6CB41E47ACCC004D91B1 /* ServiceError.swift in Sources */, - B6D2EB1F26D7B7E900F99B35 /* RandomIndexGenerator.swift in Sources */, - B6F0703A1E4FC03700F66918 /* Money.swift in Sources */, - B6FA6CCB1E4E7AEA004D91B1 /* Alignment.swift in Sources */, - B698515E1F1171EC006A537A /* SpeciesNames.swift in Sources */, - B6A29EB51EFE9B9F00DAB40C /* Currencies.swift in Sources */, - B66AF9051EAE88C300C15F8E /* Configuration.swift in Sources */, - B6CF53CE1E54DF4500CADD9F /* JSONFile.swift in Sources */, - B6C3076E2EAFBEC10066D9F0 /* CreatureSize.swift in Sources */, - B6CF53D21E54E2EF00CADD9F /* Species.swift in Sources */, - B6FA6CB01E47ACA1004D91B1 /* UnitCurrency.swift in Sources */, - B6CF53901E51DA1300CADD9F /* ClassTraits.swift in Sources */, - B621A3991F0C06A700E55236 /* NameGenerator.swift in Sources */, - B6688B512EACF5B7000A83DD /* Initiative.swift in Sources */, - B681B71E1EAACA9B001DE78B /* Die.swift in Sources */, - B64C369621756BC300C4F6BE /* DiceRoll.swift in Sources */, - B620560E1E19DDD0002494AB /* Dice.swift in Sources */, - B6CF53D01E54E2E200CADD9F /* Classes.swift in Sources */, - B620560F1E19DDD0002494AB /* DiceParser.swift in Sources */, - B6FA6CBB1E47B7F6004D91B1 /* Height.swift in Sources */, - B6F070381E4FBD6500F66918 /* SpeciesTraits.swift in Sources */, - B681B71A1EAAC8FB001DE78B /* CompoundDice.swift in Sources */, - B6FA6CBD1E47B803004D91B1 /* Weight.swift in Sources */, - B6FA6CC51E4A96D1004D91B1 /* Ability.swift in Sources */, - B626FA592EAE76AE00359F01 /* Skill.swift in Sources */, - B681B7181EAAC8EE001DE78B /* DiceModifier.swift in Sources */, - B626FA5B2EAE803000359F01 /* BackgroundTraits.swift in Sources */, - B69F84691E58B8F700A4D2B0 /* Player.swift in Sources */, - B626FA5D2EAE81C900359F01 /* Backgrounds.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B62055F31E19DD23002494AB /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6CF53E51E57429D00CADD9F /* JSONFileTests.swift in Sources */, - B6CF53D91E56442800CADD9F /* SpeciesTests.swift in Sources */, - B6F0703C1E4FC18500F66918 /* SpeciesTraitsTests.swift in Sources */, - B6F4AA4A1F12CD2A000C72D2 /* CharacterGeneratorTests.swift in Sources */, - B69F846F1E59155A00A4D2B0 /* PlayersTests.swift in Sources */, - B62D89C41F09A3870095D587 /* DiceParserTests.swift in Sources */, - B6D226F82EB25ABF00939968 /* TestBundleClass.swift in Sources */, - B626FA642EAF9FCF00359F01 /* BackgroundsTests.swift in Sources */, - B6CF53DB1E56443A00CADD9F /* ClassesTests.swift in Sources */, - B621A3981F0C052D00E55236 /* NameGeneratorTests.swift in Sources */, - B6CF53921E51DEDD00CADD9F /* ClassTraitsTests.swift in Sources */, - B6FA6CB91E47B4B1004D91B1 /* ServiceErrorTests.swift in Sources */, - B67450E41F118A8B0061FD6F /* SpeciesNamesTests.swift in Sources */, - B6F070361E4F991D00F66918 /* AlignmentTests.swift in Sources */, - B6FA6CC91E4BF5F9004D91B1 /* AbilityTests.swift in Sources */, - B6FA6CB61E47B080004D91B1 /* CurrencyTests.swift in Sources */, - B620560A1E19DDC0002494AB /* DiceTests.swift in Sources */, - B6FA6CBF1E47C2F9004D91B1 /* HeightTests.swift in Sources */, - B6FA6CC11E47C306004D91B1 /* WeightTests.swift in Sources */, - B66AF9071EAE88FF00C15F8E /* ConfigurationTests.swift in Sources */, - B69F846B1E58D33900A4D2B0 /* PlayerTests.swift in Sources */, - B6E18DF62EB26B9B000B3C90 /* DiceEncodingTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - B62055FA1E19DD23002494AB /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = B62055ED1E19DD23002494AB /* RolePlayingCore */; - targetProxy = B62055F91E19DD23002494AB /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - B62056001E19DD23002494AB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - B62056011E19DD23002494AB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - B62056031E19DD23002494AB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; - INFOPLIST_FILE = RolePlayingCore/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.RolePlayingCore; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - B62056041E19DD23002494AB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = ""; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_MODULE_VERIFIER = YES; - INFOPLIST_FILE = RolePlayingCore/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.RolePlayingCore; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - B62056061E19DD23002494AB /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = RolePlayingCoreTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.RolePlayingCoreTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - B62056071E19DD23002494AB /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEVELOPMENT_TEAM = J69E69SP27; - INFOPLIST_FILE = RolePlayingCoreTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.RolePlayingCoreTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - B62055E81E19DD23002494AB /* Build configuration list for PBXProject "RolePlayingCore" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B62056001E19DD23002494AB /* Debug */, - B62056011E19DD23002494AB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B62056021E19DD23002494AB /* Build configuration list for PBXNativeTarget "RolePlayingCore" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B62056031E19DD23002494AB /* Debug */, - B62056041E19DD23002494AB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - B62056051E19DD23002494AB /* Build configuration list for PBXNativeTarget "RolePlayingCoreTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B62056061E19DD23002494AB /* Debug */, - B62056071E19DD23002494AB /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = B62055E51E19DD23002494AB /* Project object */; -} diff --git a/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme b/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme deleted file mode 100644 index 717f947..0000000 --- a/RolePlayingCore/RolePlayingCore.xcodeproj/xcshareddata/xcschemes/RolePlayingCore.xcscheme +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/RolePlayingCore/RolePlayingCore/Info.plist b/RolePlayingCore/RolePlayingCore/Info.plist deleted file mode 100644 index e6f4652..0000000 --- a/RolePlayingCore/RolePlayingCore/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 0.1 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/RolePlayingCore/RolePlayingCore/Player/RacialNames.swift b/RolePlayingCore/RolePlayingCore/Player/RacialNames.swift deleted file mode 100644 index 181af93..0000000 --- a/RolePlayingCore/RolePlayingCore/Player/RacialNames.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// RacialNames.swift -// RolePlayingCore -// -// Created by Brian Arnold on 7/8/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import Foundation - -public struct RacialNames: Codable { - - struct FamilyNames: Codable { - let familyType: String - let maleNames: [String] - let femaleNames: [String] - let familyNames: [String]? - let childNames: [String]? - let ethnicities: [String]? - let nicknames: [String]? - let aliases: [String]? - - private enum CodingKeys: String, CodingKey { - case familyType = "family type" - case maleNames = "Male" - case femaleNames = "Female" - case familyNames = "family" - case childNames = "child" - case ethnicities - case nicknames - case aliases - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let familyType: String - let maleNames: [String] - let femaleNames: [String] - - let aliases = try container.decodeIfPresent([String].self, forKey: .aliases) - if aliases != nil { - familyType = "" - maleNames = [] - femaleNames = [] - } else { - familyType = try container.decode(String.self, forKey: .familyType) - maleNames = try container.decode([String].self, forKey: .maleNames) - femaleNames = try container.decode([String].self, forKey: .femaleNames) - } - let familyNames = try container.decodeIfPresent([String].self, forKey: .familyNames) - let childNames = try container.decodeIfPresent([String].self, forKey: .childNames) - let ethnicities = try container.decodeIfPresent([String].self, forKey: .ethnicities) - let nicknames = try container.decodeIfPresent([String].self, forKey: .nicknames) - - self.familyType = familyType - self.maleNames = maleNames - self.femaleNames = femaleNames - self.familyNames = familyNames - self.childNames = childNames - self.ethnicities = ethnicities - self.nicknames = nicknames - self.aliases = aliases - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(familyType, forKey: .familyType) - try container.encode(maleNames, forKey: .maleNames) - try container.encode(femaleNames, forKey: .femaleNames) - try container.encodeIfPresent(familyNames, forKey: .familyNames) - try container.encodeIfPresent(childNames, forKey: .childNames) - try container.encodeIfPresent(ethnicities, forKey: .ethnicities) - try container.encodeIfPresent(nicknames, forKey: .nicknames) - try container.encodeIfPresent(aliases, forKey: .aliases) - } - } - - let names: [String: FamilyNames] - - - static let randomNumberGenerator: RandomNumberGenerator = DefaultRandomNumberGenerator() - - func resolveRacialNames(_ racialTraits: RacialTraits) -> FamilyNames { - guard names[racialTraits.name] == nil, let parentName = racialTraits.parentName else { return names[racialTraits.name]! } - - return names[parentName]! - } - - func resolveAliasNames(_ familyNames: FamilyNames) -> FamilyNames { - guard let aliases = familyNames.aliases else { return familyNames } - - let index = RacialNames.randomNumberGenerator.random(aliases.count) - let randomName = aliases[index] - return names[randomName]! - } - - func resolveGender(_ gender: Player.Gender?) -> Player.Gender { - guard gender == nil else { return gender! } - return RacialNames.randomNumberGenerator.random(2) == 0 ? .male : .female - } - - public func randomName(racialTraits: RacialTraits, gender: Player.Gender?) -> String { - // Determine race or parent race (for subraces) - var familyNames = resolveRacialNames(racialTraits) - familyNames = resolveAliasNames(familyNames) - - let gender = resolveGender(gender) - let genderNames = gender == .male ? familyNames.maleNames : familyNames.femaleNames - let nameGenerator = NameGenerator(genderNames) - return nameGenerator.makeName() - } - - // TODO: family names, child names, nicknames -} diff --git a/RolePlayingCore/RolePlayingCore/Player/Skill.swift b/RolePlayingCore/RolePlayingCore/Player/Skill.swift deleted file mode 100644 index 04b0cfe..0000000 --- a/RolePlayingCore/RolePlayingCore/Player/Skill.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// Skill.swift -// RolePlayingCore -// -// Created by Brian Arnold on 10/26/25. -// Copyright © 2025 Brian Arnold. All rights reserved. -// - -public struct Skill { - public let name: String - public let ability: Ability -} - -extension Skill: Codable { } - -extension Skill: Hashable { } - -extension Skill { - public static let acrobatics = Skill(name: "Acrobatics", ability: .dexterity) - public static let animalHandling = Skill(name: "Animal Handling", ability: .wisdom) - public static let arcana = Skill(name: "Arcana", ability: .intelligence) - public static let athletics = Skill(name: "Athletics", ability: .strength) - public static let deception = Skill(name: "Deception", ability: .charisma) - public static let history = Skill(name: "History", ability: .intelligence) - public static let insight = Skill(name: "Insight", ability: .wisdom) - public static let intimidation = Skill(name: "Intimidation", ability: .charisma) - public static let investigation = Skill(name: "Investigation", ability: .intelligence) - public static let medicine = Skill(name: "Medicine", ability: .wisdom) - public static let nature = Skill(name: "Nature", ability: .intelligence) - public static let perception = Skill(name: "Perception", ability: .wisdom) - public static let performance = Skill(name: "Performance", ability: .charisma) - public static let persuasion = Skill(name: "Persuasion", ability: .charisma) - public static let religion = Skill(name: "Religion", ability: .intelligence) - public static let sleightOfHand = Skill(name: "Sleight of Hand", ability: .dexterity) - public static let stealth = Skill(name: "Stealth", ability: .dexterity) - public static let survival = Skill(name: "Survival", ability: .wisdom) - - public static var all: [Skill] = [ - .acrobatics, .animalHandling, .arcana, .athletics, .deception, .history, .insight, .intimidation, .investigation, .medicine, .nature, .perception, .performance, .persuasion, .religion, .sleightOfHand, .stealth, .survival - ] - - public static func skills(from names: [String]) -> [Skill] { - // Use the full set of skills if the names are empty. - guard names.count > 0 else { return all } - return all.filter { names.contains($0.name) } - } -} - -extension Sequence where Element == Skill { - - /// Returns a random array of skills with the specified skill count. - public func randomSkills(count: Int) -> [Element] { - var selected: [Element] = [] - var remaining: [Element] = Array(self) - - for _ in 0.. - -//! Project version number for RolePlayingCore. -FOUNDATION_EXPORT double RolePlayingCoreVersionNumber; - -//! Project version string for RolePlayingCore. -FOUNDATION_EXPORT const unsigned char RolePlayingCoreVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/RolePlayingCore/RolePlayingCoreTests/Info.plist b/RolePlayingCore/RolePlayingCoreTests/Info.plist deleted file mode 100644 index 6c6c23c..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/RolePlayingCore/RolePlayingCoreTests/RacialNamesTests.swift b/RolePlayingCore/RolePlayingCoreTests/RacialNamesTests.swift deleted file mode 100644 index 306b19c..0000000 --- a/RolePlayingCore/RolePlayingCoreTests/RacialNamesTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SpeciesNamesTests.swift -// RolePlayingCoreTests -// -// Created by Brian Arnold on 7/8/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import XCTest - -@testable import RolePlayingCore - -class SpeciesNamesTests: XCTestCase { - - func testRacialNames() { - let bundle = Bundle(for: SpeciesNamesTests.self) - do { - let data = try bundle.loadJSON("TestRacialNames") - let decoder = JSONDecoder() - let speciesNames = try decoder.decode(RacialNames.self, from: data) - - XCTAssertEqual(speciesNames.names.count, 10, "Number of species name families") - - // TODO: find a way to test just the minimum functionality. - // In the meantime, use the test species. - let bundle = Bundle(for: SpeciesNamesTests.self) - let jsonData = try bundle.loadJSON("TestRaces") - let species = try decoder.decode(Species.self, from: jsonData) - let moreJsonData = try bundle.loadJSON("TestMoreRaces") - let moreRaces = try decoder.decode(Species.self, from: moreJsonData) - - let allRaces = Species() - allRaces.species = species.species + moreRaces.species - - // TODO: random names are hard; for now, get code coverage. - do { - _ = speciesNames.randomName(speciesTraits: allRaces.find("Human")!, gender: .female) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Elf")!, gender: .male) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Mountain Dwarf")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Stout")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Half-Elf")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Half-Orc")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Dragonborn")!, gender: nil) - _ = speciesNames.randomName(speciesTraits: allRaces.find("Tiefling")!, gender: nil) - - } - - let encoder = JSONEncoder() - _ = try encoder.encode(speciesNames) - } - catch let error { - XCTFail("error thrown: \(error)") - } - } -} diff --git a/RolePlayingCore/RolePlayingCore/Common/Height.swift b/Sources/RolePlayingCore/Common/Height.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Common/Height.swift rename to Sources/RolePlayingCore/Common/Height.swift diff --git a/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift b/Sources/RolePlayingCore/Common/JSONFile.swift similarity index 97% rename from RolePlayingCore/RolePlayingCore/Common/JSONFile.swift rename to Sources/RolePlayingCore/Common/JSONFile.swift index 5fde359..7307efd 100644 --- a/RolePlayingCore/RolePlayingCore/Common/JSONFile.swift +++ b/Sources/RolePlayingCore/Common/JSONFile.swift @@ -6,6 +6,7 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // +import Foundation extension Bundle { diff --git a/RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift b/Sources/RolePlayingCore/Common/NameGenerator.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Common/NameGenerator.swift rename to Sources/RolePlayingCore/Common/NameGenerator.swift diff --git a/RolePlayingCore/RolePlayingCore/Common/RandomIndexGenerator.swift b/Sources/RolePlayingCore/Common/RandomIndexGenerator.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Common/RandomIndexGenerator.swift rename to Sources/RolePlayingCore/Common/RandomIndexGenerator.swift diff --git a/RolePlayingCore/RolePlayingCore/Common/ServiceError.swift b/Sources/RolePlayingCore/Common/ServiceError.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Common/ServiceError.swift rename to Sources/RolePlayingCore/Common/ServiceError.swift diff --git a/RolePlayingCore/RolePlayingCore/Common/SpeciesNames.swift b/Sources/RolePlayingCore/Common/SpeciesNames.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Common/SpeciesNames.swift rename to Sources/RolePlayingCore/Common/SpeciesNames.swift diff --git a/RolePlayingCore/RolePlayingCore/Common/Weight.swift b/Sources/RolePlayingCore/Common/Weight.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Common/Weight.swift rename to Sources/RolePlayingCore/Common/Weight.swift diff --git a/RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift b/Sources/RolePlayingCore/Configuration/CharacterGenerator.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Configuration/CharacterGenerator.swift rename to Sources/RolePlayingCore/Configuration/CharacterGenerator.swift diff --git a/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift b/Sources/RolePlayingCore/Configuration/Configuration.swift similarity index 93% rename from RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift rename to Sources/RolePlayingCore/Configuration/Configuration.swift index 09df5d6..b6c07c6 100644 --- a/RolePlayingCore/RolePlayingCore/Configuration/Configuration.swift +++ b/Sources/RolePlayingCore/Configuration/Configuration.swift @@ -14,6 +14,7 @@ import Foundation public struct ConfigurationFiles: Decodable { let currencies: [String] + let skills: [String] let backgrounds: [String] let species: [String] let classes: [String] @@ -22,6 +23,7 @@ public struct ConfigurationFiles: Decodable { private enum CodingKeys: String, CodingKey { case currencies + case skills case backgrounds case species case classes @@ -37,6 +39,7 @@ public struct Configuration { public var configurationFiles: ConfigurationFiles public var backgrounds = Backgrounds() + public var skills = Skills() public var species = Species() public var classes = Classes() public var players = Players() @@ -56,7 +59,12 @@ public struct Configuration { let jsonData = try bundle.loadJSON(currenciesFile) _ = try jsonDecoder.decode(Currencies.self, from: jsonData) } - + + for skillsFile in configurationFiles.skills { + let jsonData = try bundle.loadJSON(skillsFile) + _ = try jsonDecoder.decode(Skills.self, from: jsonData) + } + for backgroundsFile in configurationFiles.backgrounds { let jsonData = try bundle.loadJSON(backgroundsFile) let backgrounds = try jsonDecoder.decode(Backgrounds.self, from: jsonData) diff --git a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift b/Sources/RolePlayingCore/Currency/Currencies.swift similarity index 86% rename from RolePlayingCore/RolePlayingCore/Currency/Currencies.swift rename to Sources/RolePlayingCore/Currency/Currencies.swift index 0155d21..1dd7745 100644 --- a/RolePlayingCore/RolePlayingCore/Currency/Currencies.swift +++ b/Sources/RolePlayingCore/Currency/Currencies.swift @@ -6,11 +6,16 @@ // Copyright © 2017 Brian Arnold. All rights reserved. // +import Foundation + public struct Currencies { /// A map of all currently loaded currencies. - internal static var allCurrencies: [String: UnitCurrency] = [:] + internal static nonisolated(unsafe) var allCurrencies: [String: UnitCurrency] = [:] + /// The default base unit is a currency called "credit". It may be replaced at runtime. + internal static nonisolated(unsafe) var baseUnitCurrency = UnitCurrency(symbol: "c", converter: UnitConverterLinear(coefficient: 1.0), name: "credit", plural: "credits") + /// A lock to protect access to allCurrencies from multiple threads. private static let lock = NSLock() @@ -32,14 +37,20 @@ public struct Currencies { defer { lock.unlock() } // Remove the old base unit from all currencies. - let oldSymbol = UnitCurrency.baseUnitCurrency.symbol + let oldSymbol = baseUnitCurrency.symbol guard oldSymbol != newBaseUnit.symbol else { return } allCurrencies[oldSymbol] = nil - UnitCurrency.baseUnitCurrency = newBaseUnit + baseUnitCurrency = newBaseUnit + } + + public static func base() -> UnitCurrency { + lock.lock() + defer { lock.unlock() } + return baseUnitCurrency } } diff --git a/RolePlayingCore/RolePlayingCore/Currency/Money.swift b/Sources/RolePlayingCore/Currency/Money.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Currency/Money.swift rename to Sources/RolePlayingCore/Currency/Money.swift diff --git a/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift b/Sources/RolePlayingCore/Currency/UnitCurrency.swift similarity index 79% rename from RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift rename to Sources/RolePlayingCore/Currency/UnitCurrency.swift index 7cb102a..27a7e87 100644 --- a/RolePlayingCore/RolePlayingCore/Currency/UnitCurrency.swift +++ b/Sources/RolePlayingCore/Currency/UnitCurrency.swift @@ -18,12 +18,9 @@ public final class UnitCurrency : Dimension, @unchecked Sendable { /// The plural unit name used when the unitStyle is long. public internal(set) var plural: String! - - /// The default base unit is a currency called "credit". It may be replaced at runtime. - internal static var baseUnitCurrency = UnitCurrency(symbol: "c", converter: UnitConverterLinear(coefficient: 1.0), name: "credit", plural: "credits") - + public override class func baseUnit() -> UnitCurrency { - return baseUnitCurrency + return Currencies.base() } public init(symbol: String, converter: UnitConverter, name: String, plural: String) { diff --git a/RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift b/Sources/RolePlayingCore/Dice/CompoundDice.swift similarity index 90% rename from RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift rename to Sources/RolePlayingCore/Dice/CompoundDice.swift index a12d389..d428da7 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/CompoundDice.swift +++ b/Sources/RolePlayingCore/Dice/CompoundDice.swift @@ -39,15 +39,15 @@ public struct CompoundDice: Dice { internal typealias MathOperator = (Int, Int) -> Int /// Mapping of strings to function signatures. - internal static let mathOperators: [String: MathOperator] = ["+": (+), "-": (-), "x": (*), "*": (*), "/": (/)] - + internal let mathOperators: [String: MathOperator] = ["+": (+), "-": (-), "x": (*), "*": (*), "/": (/)] + /// Rolls the specified number of times, optionally adding or multiplying a modifier, /// and returning the result. public func roll() -> DiceRoll { let lhsRoll = lhs.roll() let rhsRoll = rhs.roll() - let result = CompoundDice.mathOperators[mathOperator]!(lhsRoll.result, rhsRoll.result) + let result = mathOperators[mathOperator]!(lhsRoll.result, rhsRoll.result) let description = "\(lhsRoll.description) \(mathOperator) \(rhsRoll.description)" return DiceRoll(result, description) diff --git a/RolePlayingCore/RolePlayingCore/Dice/Dice.swift b/Sources/RolePlayingCore/Dice/Dice.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Dice/Dice.swift rename to Sources/RolePlayingCore/Dice/Dice.swift diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift b/Sources/RolePlayingCore/Dice/DiceModifier.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Dice/DiceModifier.swift rename to Sources/RolePlayingCore/Dice/DiceModifier.swift diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift b/Sources/RolePlayingCore/Dice/DiceParser.swift similarity index 99% rename from RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift rename to Sources/RolePlayingCore/Dice/DiceParser.swift index 3d43b23..d7c61fb 100644 --- a/RolePlayingCore/RolePlayingCore/Dice/DiceParser.swift +++ b/Sources/RolePlayingCore/Dice/DiceParser.swift @@ -32,7 +32,7 @@ internal enum Token { case die case drop(String) - static let mathOperatorCharacters = CharacterSet(charactersIn: CompoundDice.mathOperators.keys.reduce("", +)) + static let mathOperatorCharacters = CharacterSet(charactersIn: "+-x*/") static let dieCharacters = CharacterSet(charactersIn: "dD") static let dropCharacters = CharacterSet(charactersIn: DroppingDice.Drop.allCases.map({ $0.rawValue }).reduce("", +)) static let percentCharacters = CharacterSet(charactersIn: "%") diff --git a/RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift b/Sources/RolePlayingCore/Dice/DiceRoll.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Dice/DiceRoll.swift rename to Sources/RolePlayingCore/Dice/DiceRoll.swift diff --git a/RolePlayingCore/RolePlayingCore/Dice/Die.swift b/Sources/RolePlayingCore/Dice/Die.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Dice/Die.swift rename to Sources/RolePlayingCore/Dice/Die.swift diff --git a/RolePlayingCore/RolePlayingCore/Dice/DroppingDice.swift b/Sources/RolePlayingCore/Dice/DroppingDice.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Dice/DroppingDice.swift rename to Sources/RolePlayingCore/Dice/DroppingDice.swift diff --git a/RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift b/Sources/RolePlayingCore/Dice/SimpleDice.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Dice/SimpleDice.swift rename to Sources/RolePlayingCore/Dice/SimpleDice.swift diff --git a/RolePlayingCore/RolePlayingCore/Player/Ability.swift b/Sources/RolePlayingCore/Player/Ability.swift similarity index 99% rename from RolePlayingCore/RolePlayingCore/Player/Ability.swift rename to Sources/RolePlayingCore/Player/Ability.swift index 3ed768a..7464354 100644 --- a/RolePlayingCore/RolePlayingCore/Player/Ability.swift +++ b/Sources/RolePlayingCore/Player/Ability.swift @@ -6,7 +6,7 @@ // Copyright © 2016-2017 Brian Arnold. All rights reserved. // -public struct Ability { +public struct Ability: Sendable { public let name: String /// Creates an ability name. diff --git a/RolePlayingCore/RolePlayingCore/Player/Alignment.swift b/Sources/RolePlayingCore/Player/Alignment.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Player/Alignment.swift rename to Sources/RolePlayingCore/Player/Alignment.swift diff --git a/RolePlayingCore/RolePlayingCore/Player/BackgroundTraits.swift b/Sources/RolePlayingCore/Player/BackgroundTraits.swift similarity index 75% rename from RolePlayingCore/RolePlayingCore/Player/BackgroundTraits.swift rename to Sources/RolePlayingCore/Player/BackgroundTraits.swift index a1dcd5e..e4ba537 100644 --- a/RolePlayingCore/RolePlayingCore/Player/BackgroundTraits.swift +++ b/Sources/RolePlayingCore/Player/BackgroundTraits.swift @@ -7,12 +7,12 @@ // public struct BackgroundTraits { - public let name: String - public let abilityScores: [String] - public let feat: String - public let skillProficiencies: [Skill] - public let toolProficiency: String - public let equipment: [[String]] + public var name: String + public var abilityScores: [String] + public var feat: String + public var skillProficiencies: [String] + public var toolProficiency: String + public var equipment: [[String]] } extension BackgroundTraits: Codable { @@ -30,9 +30,7 @@ extension BackgroundTraits: Codable { self.name = try values.decode(String.self, forKey: .name) self.abilityScores = try values.decode([String].self, forKey: .abilityScores) self.feat = try values.decode(String.self, forKey: .feat) - - let skillNames = try values.decode([String].self, forKey: .skillProficiencies) - self.skillProficiencies = Skill.skills(from: skillNames) + self.skillProficiencies = try values.decode([String].self, forKey: .skillProficiencies) self.toolProficiency = try values.decode(String.self, forKey: .toolProficiency) self.equipment = try values.decode([[String]].self, forKey: .equipment) } @@ -42,7 +40,7 @@ extension BackgroundTraits: Codable { try container.encode(name, forKey: .name) try container.encode(abilityScores, forKey: .abilityScores) try container.encode(feat, forKey: .feat) - try container.encode(skillProficiencies.skillNames, forKey: .skillProficiencies) + try container.encode(skillProficiencies, forKey: .skillProficiencies) try container.encode(toolProficiency, forKey: .toolProficiency) try container.encode(equipment, forKey: .equipment) } diff --git a/RolePlayingCore/RolePlayingCore/Player/Backgrounds.swift b/Sources/RolePlayingCore/Player/Backgrounds.swift similarity index 100% rename from RolePlayingCore/RolePlayingCore/Player/Backgrounds.swift rename to Sources/RolePlayingCore/Player/Backgrounds.swift diff --git a/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift b/Sources/RolePlayingCore/Player/ClassTraits.swift similarity index 93% rename from RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift rename to Sources/RolePlayingCore/Player/ClassTraits.swift index 8e297ed..c6e1e2b 100644 --- a/RolePlayingCore/RolePlayingCore/Player/ClassTraits.swift +++ b/Sources/RolePlayingCore/Player/ClassTraits.swift @@ -25,7 +25,7 @@ public struct ClassTraits { public var toolProficiencies: [String] public var armorTraining: [String] public var startingEquipment: [[String]] - + /// Accesses the experiencePoints array for the specified 1-based level. public func minExperiencePoints(at level: Int) -> Int { // Map the level to an index of the array @@ -169,3 +169,20 @@ extension ClassTraits: Codable { try values.encodeIfPresent(experiencePoints, forKey: .experiencePoints) } } + +extension ClassTraits { + + /// Returns a random array of skill proficiencies, of a count matching startingSkillCount. + public func randomSkillProficiencies() -> [String] { + var selected: [String] = [] + var remaining: [String] = skillProficiencies + + for _ in 0.. Skill? { + return skills.first(where: { $0.name == skillName }) + } + + public var count: Int { return skills.count } + + public subscript(index: Int) -> Skill? { + get { + return skills[index] + } + } +} + +extension Sequence where Element == Skill { + + /// Returns a random array of skills with the specified skill count. + public func randomSkills(count: Int) -> [Element] { + var selected: [Element] = [] + var remaining: [Element] = Array(self) + + for _ in 0.. Date: Thu, 30 Oct 2025 12:35:17 -0400 Subject: [PATCH 11/33] Changed the macOS target to Catalyst. --- .../CharacterGenerator.xcodeproj/project.pbxproj | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj index d19311e..fa5b59d 100644 --- a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj @@ -420,14 +420,15 @@ IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; SDKROOT = auto; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; @@ -464,14 +465,15 @@ IPHONEOS_DEPLOYMENT_TARGET = 26.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 26.0; + MACOSX_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.flatearthstudio.CharacterGenerator; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; SDKROOT = auto; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; From 09cf7fe4ee14d7bd9fa2d880482e388007e25da8 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Thu, 30 Oct 2025 12:44:02 -0400 Subject: [PATCH 12/33] Renamed the examples workspace. --- .../contents.xcworkspacedata | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Examples/CharacterGenerator/{CharacterGenerator.xcworkspace => Examples.xcworkspace}/contents.xcworkspacedata (100%) diff --git a/Examples/CharacterGenerator/CharacterGenerator.xcworkspace/contents.xcworkspacedata b/Examples/CharacterGenerator/Examples.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Examples/CharacterGenerator/CharacterGenerator.xcworkspace/contents.xcworkspacedata rename to Examples/CharacterGenerator/Examples.xcworkspace/contents.xcworkspacedata From 1d8f010f61f21ece50bafe1c73ebec3515c48081 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Thu, 30 Oct 2025 18:38:29 -0400 Subject: [PATCH 13/33] Updated the ReadMe. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a1a81a0..b5e00f7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ This framework provides reusable role playing game core logic in the Swift language. It is a work-in-progress. Capabilities will be provided incrementally over time. -The short-term goal for this project is to provide core logic for implementing a role playing game on macOS, iOS and Linux. The architecture is intended to be flexible enough to support Open Game Content in addition to similar kinds of games, and nimble enough to minimize upstream dependencies. The iOS platform in framework library format is being targeted first because this provides the most restrictive environment for implementing a library intended for reuse. The longer-term goal is to leverage this as a framework or library for implementing role playing games and utilities on the desktop and web. +The short-term goal for this project is to provide core logic for implementing a role playing game on macOS, iOS, and Linux. The architecture is intended to be flexible enough to support Open Game Content in addition to similar kinds of games, and nimble enough to minimize upstream dependencies. + +The library is built as a generic Swift Package, and the Example character generator app uses the iOS SDK with SwiftUI. The longer-term goal is to leverage this as a framework or library for implementing role playing games and utilities on the desktop and web. ## Requirements @@ -12,8 +14,8 @@ Xcode 26 or Swift 6 are required. The current organizational groupings include: -* **Common**: common utilities such as height and weight, and a runtime error enum -* **Currency**: currency types, conversion and parsing +* **Common**: common utilities such as height and weight, and a runtime error type +* **Currency**: currency types, conversion, and parsing * **Dice**: dice types and parsing * **Player**: the player, species, classes, and related types From 83698ed55995ed30124998e28ac7a00985fafae8 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Thu, 30 Oct 2025 18:51:10 -0400 Subject: [PATCH 14/33] Create swift.yml --- .github/workflows/swift.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/swift.yml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..350f4ae --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,22 @@ +# This workflow will build a Swift project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift + +name: Swift + +on: + push: + branches: [ "development" ] + pull_request: + branches: [ "development" ] + +jobs: + build: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v From cc1d9c794e08f055bc20961ef85e1a094c0b4313 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Thu, 30 Oct 2025 18:56:32 -0400 Subject: [PATCH 15/33] Delete .travis.yml --- .travis.yml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 12e88bb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: swift -osx_image: xcode10 -script: -- set -o pipefail -- travis_retry xcodebuild -workspace RolePlayingCore.xcworkspace -scheme "RolePlayingCore" -destination "platform=iOS Simulator,name=iPhone 8" analyze | xcpretty -- travis_retry xcodebuild -workspace RolePlayingCore.xcworkspace -scheme "CharacterGenerator" -destination "platform=iOS Simulator,name=iPhone 8" -enableCodeCoverage NO build-for-testing test | xcpretty -- travis_retry xcodebuild -workspace RolePlayingCore.xcworkspace -scheme "RolePlayingCore" -destination "platform=iOS Simulator,name=iPhone 8" -enableCodeCoverage YES build-for-testing test | xcpretty -after_success: - - bash <(curl -s https://codecov.io/bash) -J 'RolePlayingCore' -cF ios -X xcodeplist From 8ecb40b861cbf4a11e9d696797d70d660ffaa61b Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Thu, 30 Oct 2025 18:57:08 -0400 Subject: [PATCH 16/33] Update codecov.yml --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index 7dc263a..87fb0be 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,3 @@ coverage: ignore: - - RolePlayingCore/RolePlayingCoreTests/* + - Tests/RolePlayingCoreTests/* From ac4d8588ea67a3937f1982bc304753ea195660d1 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Fri, 7 Nov 2025 08:57:17 -0500 Subject: [PATCH 17/33] Added some comments for documentation and improved clarity of some functions. --- Sources/RolePlayingCore/Common/Height.swift | 115 +++++++++++------- Sources/RolePlayingCore/Common/JSONFile.swift | 23 ---- .../RolePlayingCore/Common/ServiceError.swift | 39 ------ .../RolePlayingCore/Common/SpeciesNames.swift | 1 + Sources/RolePlayingCore/Common/Weight.swift | 47 ++++--- .../Configuration/CharacterGenerator.swift | 2 +- .../Configuration/Configuration.swift | 8 +- .../Configuration/ConfigurationError.swift | 56 +++++++++ .../Configuration/JSONFile.swift | 26 ++++ .../RolePlayingCore/Currency/Currencies.swift | 18 +-- Sources/RolePlayingCore/Currency/Money.swift | 5 +- Sources/RolePlayingCore/Player/Ability.swift | 4 +- .../Player/BackgroundTraits.swift | 1 + .../RolePlayingCore/Player/Backgrounds.swift | 2 +- .../RolePlayingCore/Player/ClassTraits.swift | 1 + Sources/RolePlayingCore/Player/Classes.swift | 2 +- .../RolePlayingCore/Player/CreatureSize.swift | 4 +- Sources/RolePlayingCore/Player/Player.swift | 13 +- Sources/RolePlayingCore/Player/Players.swift | 7 +- Sources/RolePlayingCore/Player/Skills.swift | 2 + Sources/RolePlayingCore/Player/Species.swift | 1 + .../Player/SpeciesTraits.swift | 1 + .../ConfigurationErrorTests.swift | 28 +++++ .../RolePlayingCoreTests/JSONFileTests.swift | 4 +- .../ServiceErrorTests.swift | 28 ----- 25 files changed, 255 insertions(+), 183 deletions(-) delete mode 100644 Sources/RolePlayingCore/Common/JSONFile.swift delete mode 100644 Sources/RolePlayingCore/Common/ServiceError.swift create mode 100644 Sources/RolePlayingCore/Configuration/ConfigurationError.swift create mode 100644 Sources/RolePlayingCore/Configuration/JSONFile.swift create mode 100644 Tests/RolePlayingCoreTests/ConfigurationErrorTests.swift delete mode 100644 Tests/RolePlayingCoreTests/ServiceErrorTests.swift diff --git a/Sources/RolePlayingCore/Common/Height.swift b/Sources/RolePlayingCore/Common/Height.swift index 8946c98..b58f21d 100644 --- a/Sources/RolePlayingCore/Common/Height.swift +++ b/Sources/RolePlayingCore/Common/Height.swift @@ -8,7 +8,7 @@ import Foundation -/// Height is a measurement of length. +/// A measurement of length. public typealias Height = Measurement extension String { @@ -16,60 +16,85 @@ extension String { /// Parses ft or ', in or ", cm, m into a measurement of length. /// Returns nil if the string could not be parsed. public var parseHeight: Height? { - var value: Double? - var unit: UnitLength = .feet - - // Try going feet-first. - let feetList = ["'", "ft"] - var feetEndRange: Range? - for key in feetList { - feetEndRange = self.range(of: key) - if let feetEndRange = feetEndRange { - value = Double(self[..(value: inches, unit: .inches).converted(to: .feet).value - value = value != nil ? value! + inchesInFeet : inchesInFeet - break - } + // Try parsing metric units + if let height = parseMetricHeight(from: trimmed) { + return height } - // If neither feet nor inches were specified, try metric. - if value == nil { - let metricMap: [String: UnitLength] = [ - "cm": .centimeters, - "m": .meters] - - for (key, metricUnit) in metricMap { - if let range = self.range(of: key) { - value = Double(self[.. Height? { + let feetMarkers = ["'", "ft"] + let inchesMarkers = ["\"", "in"] + + // Look for feet marker + var feetValue: Double = 0 + var feetEndIndex = string.startIndex + var foundFeet = false + + for marker in feetMarkers { + if let range = string.range(of: marker) { + let feetString = string[.. Height? { + let metricUnits: [(marker: String, unit: UnitLength)] = [ + ("cm", .centimeters), + ("m", .meters) + ] + + for (marker, unit) in metricUnits { + if let range = string.range(of: marker) { + let valueString = string[.. Data { - guard let url = self.url(forResource: fileName, withExtension: "json") else { throw RuntimeError("Could not load \(fileName).json from \(self.bundleURL)") } - return try Data(contentsOf: url, options: [.mappedIfSafe]) - } -} diff --git a/Sources/RolePlayingCore/Common/ServiceError.swift b/Sources/RolePlayingCore/Common/ServiceError.swift deleted file mode 100644 index ab5335d..0000000 --- a/Sources/RolePlayingCore/Common/ServiceError.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ServiceError.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/5/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -/* - TODO: Consider switching to Swift Foundation error types if they become available. - - In the meantime, define "std::exception"-style errors manually from Error protocol. - */ - -public enum ServiceError: Error { - - /// Represents an error detected at runtime. - case runtimeError(String, String) - -} - -/// Returns a ServiceError.runtimeError with the specified message. -/// The source function, file and line are recorded in location. -public func RuntimeError(_ message: String, source: String = #function, file: String = #file, line: Int = #line) -> ServiceError { - return ServiceError.runtimeError(message, "Thrown in \(source) (File: \(file) Line: \(line))") -} - -extension ServiceError: CustomStringConvertible { - - /// Returns a description of the error, including the type, message, - /// source function, file and line. - public var description: String { - switch self { - case .runtimeError(let message, let location): - return "Runtime error: \(message)\n\(location)" - } - } - -} diff --git a/Sources/RolePlayingCore/Common/SpeciesNames.swift b/Sources/RolePlayingCore/Common/SpeciesNames.swift index 57ca7f2..6a712a8 100644 --- a/Sources/RolePlayingCore/Common/SpeciesNames.swift +++ b/Sources/RolePlayingCore/Common/SpeciesNames.swift @@ -8,6 +8,7 @@ import Foundation +/// Collections of species names, for use in generating random names. public struct SpeciesNames: Codable { struct FamilyNames: Codable { diff --git a/Sources/RolePlayingCore/Common/Weight.swift b/Sources/RolePlayingCore/Common/Weight.swift index fd84c89..04088a3 100644 --- a/Sources/RolePlayingCore/Common/Weight.swift +++ b/Sources/RolePlayingCore/Common/Weight.swift @@ -15,29 +15,36 @@ extension String { /// Parses "lb" or "kg" into a measurement of mass. public var parseWeight: Weight? { - var value: Double? - var unit: UnitMass = .pounds + let trimmed = self.trimmingCharacters(in: .whitespaces) - let weightMap: [String: UnitMass] = [ - "lb": .pounds, - "kg": .kilograms] - - for (key, weightUnit) in weightMap { - if let range = self.range(of: key) { - value = Double(self[.. Weight? { + let weightUnits: [(marker: String, unit: UnitMass)] = [ + ("lb", .pounds), + ("kg", .kilograms) + ] - return Weight(value: value!, unit: unit) + for (marker, unit) in weightUnits { + if let range = string.range(of: marker) { + let valueString = string[.. Error { + return ConfigurationError.missingFile(fileName, bundlePath, "Thrown in \(source) (File: \(file) Line: \(line))") +} + +/// Returns a badJSON ConfigurationError with the specified message. +/// The source function, file and line are recorded in location. +public func missingJSONError(_ name: String, source: String = #function, file: String = #file, line: Int = #line) -> Error { + return ConfigurationError.missingJSON(name, "Thrown in \(source) (File: \(file) Line: \(line))") +} + +/// Returns a missingType ConfigurationError with the specified message. +/// The source function, file and line are recorded in location. +public func missingTypeError(_ kind: String, _ name: String, source: String = #function, file: String = #file, line: Int = #line) -> Error { + return ConfigurationError.missingType(kind, name, "Thrown in \(source) (File: \(file) Line: \(line))") +} + +extension ConfigurationError: CustomStringConvertible { + + private static let prefix = "Configuration error" + + /// Returns a description of the error, including the type, message, + /// source function, file and line. + public var description: String { + switch self { + case .missingFile(let fileName, let bundlePath, let location): + return "\(Self.prefix): Missing file \(fileName) in bundle \(bundlePath)\n\(location)" + case .missingJSON(let name, let location): + return "\(Self.prefix): Missing \(name) in configuration file\n\(location)" + case .missingType(let kind, let name, let location): + return "\(Self.prefix): Could not resolve \(kind) named \(name)\n\(location)" + } + } + +} diff --git a/Sources/RolePlayingCore/Configuration/JSONFile.swift b/Sources/RolePlayingCore/Configuration/JSONFile.swift new file mode 100644 index 0000000..3018d4b --- /dev/null +++ b/Sources/RolePlayingCore/Configuration/JSONFile.swift @@ -0,0 +1,26 @@ +// +// JSONFile.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/15/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Foundation + +extension Bundle { + + /// Loads a specified JSON-formatted UTF-8 file from this bundle's resources, and returns its dictionary. + /// + /// - parameter fileName: The name of the file, without its extension. + /// - parameter source: The name of the calling function. Defaults to `#function`. + /// - parameter file: The name of the file where the error occurred. Defaults to `#file`. + /// - parameter line: The line number where the error occurred. Defaults to `#line`. + /// + /// - throws: `ConfigurationError.missingFile` if the file is missing. + /// - throws: `NSError` if the file can't be read. + public func loadJSON(_ fileName: String, source: String = #function, file: String = #file, line: Int = #line) throws -> Data { + guard let url = self.url(forResource: fileName, withExtension: "json") else { throw missingFileError("\(fileName).json", "\(self.bundleURL)", source: source, file: file, line: line) } + return try Data(contentsOf: url, options: [.mappedIfSafe]) + } +} diff --git a/Sources/RolePlayingCore/Currency/Currencies.swift b/Sources/RolePlayingCore/Currency/Currencies.swift index 1dd7745..f6d87c3 100644 --- a/Sources/RolePlayingCore/Currency/Currencies.swift +++ b/Sources/RolePlayingCore/Currency/Currencies.swift @@ -8,13 +8,14 @@ import Foundation +/// A collection of currencies. public struct Currencies { /// A map of all currently loaded currencies. - internal static nonisolated(unsafe) var allCurrencies: [String: UnitCurrency] = [:] + internal private(set) static nonisolated(unsafe) var allCurrencies: [String: UnitCurrency] = [:] /// The default base unit is a currency called "credit". It may be replaced at runtime. - internal static nonisolated(unsafe) var baseUnitCurrency = UnitCurrency(symbol: "c", converter: UnitConverterLinear(coefficient: 1.0), name: "credit", plural: "credits") + fileprivate static nonisolated(unsafe) var baseUnitCurrency = UnitCurrency(symbol: "c", converter: UnitConverterLinear(coefficient: 1.0), name: "credit", plural: "credits") /// A lock to protect access to allCurrencies from multiple threads. private static let lock = NSLock() @@ -26,13 +27,15 @@ public struct Currencies { return Currencies.allCurrencies[symbol] } + /// Adds the unit currency to the collection of currencies. public static func add(_ currency: UnitCurrency) { lock.lock() defer { lock.unlock() } allCurrencies[currency.symbol] = currency } - public static func setDefault(_ newBaseUnit: UnitCurrency) { + /// Makes this unit currency the default for `Money`. + fileprivate static func setDefault(_ newBaseUnit: UnitCurrency) { lock.lock() defer { lock.unlock() } @@ -47,7 +50,7 @@ public struct Currencies { baseUnitCurrency = newBaseUnit } - public static func base() -> UnitCurrency { + internal static func base() -> UnitCurrency { lock.lock() defer { lock.unlock() } return baseUnitCurrency @@ -56,8 +59,8 @@ public struct Currencies { extension Currencies: Codable { - /// TODO: Codable and NSCoding haven't yet converged. In the meantime, - /// mirror UnitCurrency, using Codable instead of NSCoding. + /// TODO: UnitCurrency's Dimension conforms to NSCoding, not Codable . To support Codable, we + /// use this type to mirror UnitCurrency, and then map it once decoded. private struct Currency: Codable { let symbol: String let coefficient: Double @@ -98,7 +101,7 @@ extension Currencies: Codable { case currencies } - /// Decodes an array of currencies, setting the default currency if present. + /// Decodes an array of currencies, setting the default currency if specified. public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -115,6 +118,7 @@ extension Currencies: Codable { } } + /// Encodes an array of currencies. public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) diff --git a/Sources/RolePlayingCore/Currency/Money.swift b/Sources/RolePlayingCore/Currency/Money.swift index 6e96da7..8993e9b 100644 --- a/Sources/RolePlayingCore/Currency/Money.swift +++ b/Sources/RolePlayingCore/Currency/Money.swift @@ -14,6 +14,7 @@ public typealias Money = Measurement public extension String { /// Parses numbers with currency symbols into money. + /// If there is no currency symbol, the number is associated with the base unit currency. var parseMoney: Money? { var value: Double? var unit: UnitCurrency = .baseUnit() @@ -32,9 +33,9 @@ public extension String { } // Bail if the value could not be parsed. - guard value != nil else { return nil } + guard let value else { return nil } - return Money(value: value!, unit: unit) + return Money(value: value, unit: unit) } } diff --git a/Sources/RolePlayingCore/Player/Ability.swift b/Sources/RolePlayingCore/Player/Ability.swift index 7464354..e0d8e98 100644 --- a/Sources/RolePlayingCore/Player/Ability.swift +++ b/Sources/RolePlayingCore/Player/Ability.swift @@ -6,6 +6,7 @@ // Copyright © 2016-2017 Brian Arnold. All rights reserved. // +/// A named ability. public struct Ability: Sendable { public let name: String @@ -49,6 +50,7 @@ extension Ability: Codable { } } +/// A dictionary of ability scores. public struct AbilityScores { var scores: [Ability: Int] @@ -100,7 +102,7 @@ extension AbilityScores: Codable { self.stringValue = stringValue } - var intValue: Int? { return nil } + var intValue: Int? { nil } init?(intValue: Int) { return nil } } diff --git a/Sources/RolePlayingCore/Player/BackgroundTraits.swift b/Sources/RolePlayingCore/Player/BackgroundTraits.swift index e4ba537..ae2dfd1 100644 --- a/Sources/RolePlayingCore/Player/BackgroundTraits.swift +++ b/Sources/RolePlayingCore/Player/BackgroundTraits.swift @@ -6,6 +6,7 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // +/// Traits associated with a player character's background. public struct BackgroundTraits { public var name: String public var abilityScores: [String] diff --git a/Sources/RolePlayingCore/Player/Backgrounds.swift b/Sources/RolePlayingCore/Player/Backgrounds.swift index 2f88fba..6182cae 100644 --- a/Sources/RolePlayingCore/Player/Backgrounds.swift +++ b/Sources/RolePlayingCore/Player/Backgrounds.swift @@ -6,7 +6,7 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // -/// A collection of background traits +/// A collection of background traits. public struct Backgrounds: Codable { public var backgrounds = [BackgroundTraits]() diff --git a/Sources/RolePlayingCore/Player/ClassTraits.swift b/Sources/RolePlayingCore/Player/ClassTraits.swift index c6e1e2b..0777490 100644 --- a/Sources/RolePlayingCore/Player/ClassTraits.swift +++ b/Sources/RolePlayingCore/Player/ClassTraits.swift @@ -8,6 +8,7 @@ import Foundation +/// Traits representing a class. public struct ClassTraits { public var name: String public var plural: String diff --git a/Sources/RolePlayingCore/Player/Classes.swift b/Sources/RolePlayingCore/Player/Classes.swift index 1498e56..29425e3 100644 --- a/Sources/RolePlayingCore/Player/Classes.swift +++ b/Sources/RolePlayingCore/Player/Classes.swift @@ -6,7 +6,7 @@ // Copyright © 2016 Brian Arnold. All rights reserved. // -// A collection of class traits +/// A collection of class traits. public struct Classes: Codable { public var classes = [ClassTraits]() diff --git a/Sources/RolePlayingCore/Player/CreatureSize.swift b/Sources/RolePlayingCore/Player/CreatureSize.swift index c495147..356c2be 100644 --- a/Sources/RolePlayingCore/Player/CreatureSize.swift +++ b/Sources/RolePlayingCore/Player/CreatureSize.swift @@ -6,6 +6,7 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // +/// A player character or monster size. public enum CreatureSize: String { case tiny case small @@ -14,6 +15,7 @@ public enum CreatureSize: String { case huge case gargantuan + /// Returns a creature size relative to the specified height. init(from height: Height) { let heightInFeet = height.converted(to: .feet) switch heightInFeet.value { @@ -26,7 +28,7 @@ public enum CreatureSize: String { } } - // Integer range in inches + /// Integer range in inches var range: Range { switch self { case .tiny: return 12..<24 diff --git a/Sources/RolePlayingCore/Player/Player.swift b/Sources/RolePlayingCore/Player/Player.swift index 6a708e4..2e32cfd 100644 --- a/Sources/RolePlayingCore/Player/Player.swift +++ b/Sources/RolePlayingCore/Player/Player.swift @@ -10,7 +10,7 @@ import Foundation public extension AbilityScores { - // Sets the ability scores to random values using 4d6-L + /// Sets the ability scores to random values using '4d6-L'. mutating func roll() { let dice = DroppingDice(.d6, times: 4, drop: .lowest) for ability in abilities { @@ -21,18 +21,23 @@ public extension AbilityScores { public extension Dice { - // Return a dice with a number of rolls corresponding to level. + /// Returns a dice with a number of rolls corresponding to level. func hitDice(level: Int) -> Dice { return SimpleDice(Die(rawValue: self.sides)!, times: level) } } -// TODO: Is this a base class for Character? What about NPC? Monster? Should we have a protocol? +/// The base class for a player character, including its background, species, class, abilities, skills, hit points, and so on. public class Player: Codable { + + /// The player's name. public var name: String - public var descriptiveTraits: [String: String] // ideals, bonds, flaws, background + + /// Descriptive traits, such as ideals, bonds, flaws, a background story, etc. + public var descriptiveTraits: [String: String] public private(set) var backgroundName: String + public private(set) var speciesName: String public private(set) var className: String diff --git a/Sources/RolePlayingCore/Player/Players.swift b/Sources/RolePlayingCore/Player/Players.swift index 39e252d..03b90b3 100644 --- a/Sources/RolePlayingCore/Player/Players.swift +++ b/Sources/RolePlayingCore/Player/Players.swift @@ -14,26 +14,27 @@ extension Player { func resolveBackgrounds(from backgrounds: Backgrounds) throws { guard let backgroundTraits = backgrounds.find(self.backgroundName) else { - throw RuntimeError("Could not resolve background name \(self.backgroundName)") + throw missingTypeError("background", self.backgroundName) } self.backgroundTraits = backgroundTraits } func resolveSpecies(from species: Species) throws { guard let speciesTraits = species.find(self.speciesName) else { - throw RuntimeError("Could not resolve species name \(self.speciesName)") + throw missingTypeError("species", self.speciesName) } self.speciesTraits = speciesTraits } func resolveClass(from classes: Classes) throws { guard let classTraits = classes.find(self.className) else { - throw RuntimeError("Could not resolve class name \(self.className)") + throw missingTypeError("class", self.className) } self.classTraits = classTraits } } +/// A collection of player characters. public class Players: Codable { public var players = [Player]() diff --git a/Sources/RolePlayingCore/Player/Skills.swift b/Sources/RolePlayingCore/Player/Skills.swift index 7e209d4..ec084fa 100644 --- a/Sources/RolePlayingCore/Player/Skills.swift +++ b/Sources/RolePlayingCore/Player/Skills.swift @@ -6,6 +6,7 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // +/// A skill proficiency associated with an ability. public struct Skill { public let name: String public let ability: Ability @@ -15,6 +16,7 @@ extension Skill: Codable { } extension Skill: Hashable { } +/// A collection of skills. public struct Skills: Codable { public var skills = [Skill]() diff --git a/Sources/RolePlayingCore/Player/Species.swift b/Sources/RolePlayingCore/Player/Species.swift index 9dd83c3..77653a3 100644 --- a/Sources/RolePlayingCore/Player/Species.swift +++ b/Sources/RolePlayingCore/Player/Species.swift @@ -8,6 +8,7 @@ import Foundation +/// A collection of species traits, including subspecies. public class Species: Codable { /// Accesses all of the species and subspecies that have been loaded. diff --git a/Sources/RolePlayingCore/Player/SpeciesTraits.swift b/Sources/RolePlayingCore/Player/SpeciesTraits.swift index 9004b27..a658182 100644 --- a/Sources/RolePlayingCore/Player/SpeciesTraits.swift +++ b/Sources/RolePlayingCore/Player/SpeciesTraits.swift @@ -8,6 +8,7 @@ import Foundation +/// Traits representing a species. public struct SpeciesTraits { public struct CreatureType: Sendable { diff --git a/Tests/RolePlayingCoreTests/ConfigurationErrorTests.swift b/Tests/RolePlayingCoreTests/ConfigurationErrorTests.swift new file mode 100644 index 0000000..8f5c48b --- /dev/null +++ b/Tests/RolePlayingCoreTests/ConfigurationErrorTests.swift @@ -0,0 +1,28 @@ +// +// ConfigurationErrorTests.swift +// RolePlayingCore +// +// Created by Brian Arnold on 2/5/17. +// Copyright © 2017 Brian Arnold. All rights reserved. +// + +import Testing +import RolePlayingCore + +@Suite("Configuration Error Tests") +struct ConfigurationErrorTests { + + @Test("Verify ConfigurationError contains expected information") + func configurationError() async throws { + do { + throw missingFileError("Foo.json", "MyBundle") + } catch { + #expect(error is ConfigurationError, "should be a configuration error") + let description = "\(error)" + #expect(description.contains("Configuration error"), "should be a configuration error") + #expect(description.contains("Foo.json"), "should contain the message") + #expect(description.contains("configurationError"), "should have throw function name in it") + #expect(description.contains("ConfigurationErrorTests"), "should have throw file name in it") + } + } +} diff --git a/Tests/RolePlayingCoreTests/JSONFileTests.swift b/Tests/RolePlayingCoreTests/JSONFileTests.swift index 31d0c44..5aa1243 100644 --- a/Tests/RolePlayingCoreTests/JSONFileTests.swift +++ b/Tests/RolePlayingCoreTests/JSONFileTests.swift @@ -65,12 +65,12 @@ struct JSONFileTests { _ = try bundle.loadJSON("MissingJSONFile") } - // Verify it's specifically a ServiceError + // Verify it's specifically a ConfigurationError do { _ = try bundle.loadJSON("MissingJSONFile") Issue.record("Should have thrown an error") } catch { - #expect(error is ServiceError, "expected ServiceError, got \(error)") + #expect(error is ConfigurationError, "expected ConfigurationError, got \(error)") } } diff --git a/Tests/RolePlayingCoreTests/ServiceErrorTests.swift b/Tests/RolePlayingCoreTests/ServiceErrorTests.swift deleted file mode 100644 index c0b3bc5..0000000 --- a/Tests/RolePlayingCoreTests/ServiceErrorTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ServiceErrorTests.swift -// RolePlayingCore -// -// Created by Brian Arnold on 2/5/17. -// Copyright © 2017 Brian Arnold. All rights reserved. -// - -import Testing -import RolePlayingCore - -@Suite("Service Error Tests") -struct ServiceErrorTests { - - @Test("Verify ServiceError contains expected information") - func serviceError() async throws { - do { - throw RuntimeError("Gah!") - } catch { - #expect(error is ServiceError, "should be a service error") - let description = "\(error)" - #expect(description.contains("Runtime error"), "should be a runtime error") - #expect(description.contains("Gah!"), "should contain the message") - #expect(description.contains("serviceError"), "should have throw function name in it") - #expect(description.contains("ServiceErrorTests"), "should have throw file name in it") - } - } -} From e6121f6e4e436a75a9284e90ce3150be5ee3f84a Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Fri, 7 Nov 2025 09:27:17 -0500 Subject: [PATCH 18/33] Used Claude to clean up the implementation. --- Sources/RolePlayingCore/Dice/DiceParser.swift | 343 +++++++++++------- 1 file changed, 221 insertions(+), 122 deletions(-) diff --git a/Sources/RolePlayingCore/Dice/DiceParser.swift b/Sources/RolePlayingCore/Dice/DiceParser.swift index d7c61fb..509fb7a 100644 --- a/Sources/RolePlayingCore/Dice/DiceParser.swift +++ b/Sources/RolePlayingCore/Dice/DiceParser.swift @@ -8,12 +8,10 @@ import Foundation -// TODO: the initial implementation performed naïve sub-string searches, and was very limited. -// This implementation uses a lightweight tokenizer and parser, and is a lot more robust. -// A smaller implementation might leverage regular expression, but may be harder to maintain. +// MARK: - Parse Errors /// Types of errors handled by this parser. -internal enum DiceParseError: Error { +enum DiceParseError: Error, LocalizedError { case invalidCharacter(String) case invalidDieSides(Int) case missingMinus @@ -23,47 +21,85 @@ internal enum DiceParseError: Error { case consecutiveNumbers case consecutiveMathOperators case consecutiveDiceExpressions + + var errorDescription: String? { + switch self { + case .invalidCharacter(let char): + return "Invalid character '\(char)' in dice expression" + case .invalidDieSides(let sides): + return "Invalid die with \(sides) sides" + case .missingMinus: + return "Drop modifier requires '-' operator" + case .missingSimpleDice: + return "Drop modifier can only be applied to simple dice" + case .missingDieSides: + return "Die specification missing number of sides" + case .missingExpression: + return "Math operator missing right-hand expression" + case .consecutiveNumbers: + return "Cannot have consecutive numbers without an operator" + case .consecutiveMathOperators: + return "Cannot have consecutive math operators" + case .consecutiveDiceExpressions: + return "Cannot have consecutive dice expressions without an operator" + } + } } +// MARK: - Token + + /// Types of tokens supported by this parser. -internal enum Token { +enum Token { case number(Int) case mathOperator(String) case die case drop(String) - static let mathOperatorCharacters = CharacterSet(charactersIn: "+-x*/") - static let dieCharacters = CharacterSet(charactersIn: "dD") - static let dropCharacters = CharacterSet(charactersIn: DroppingDice.Drop.allCases.map({ $0.rawValue }).reduce("", +)) - static let percentCharacters = CharacterSet(charactersIn: "%") + // MARK: Character Sets + + private static let mathOperatorCharacters = CharacterSet(charactersIn: "+-x*/") + private static let dieCharacters = CharacterSet(charactersIn: "dD") + private static let dropCharacters = CharacterSet( + charactersIn: DroppingDice.Drop.allCases.map(\.rawValue).joined() + ) + private static let percentCharacters = CharacterSet(charactersIn: "%") + + // MARK: Initialization init?(from scalar: UnicodeScalar) { - if Token.mathOperatorCharacters.contains(scalar) { + switch scalar { + case _ where Self.mathOperatorCharacters.contains(scalar): self = .mathOperator(String(scalar)) - } else if Token.dieCharacters.contains(scalar) { + case _ where Self.dieCharacters.contains(scalar): self = .die - } else if Token.dropCharacters.contains(scalar) { + case _ where Self.dropCharacters.contains(scalar): self = .drop(String(scalar)) - } else if Token.percentCharacters.contains(scalar) { + case _ where Self.percentCharacters.contains(scalar): self = .number(100) - } else { + default: return nil } } + // MARK: Properties + var isDropping: Bool { - // TODO: is this the most compact way to compare an enum? - guard case .drop(_) = self else { return false } - return true + if case .drop = self { return true } + return false } } +// MARK: - Number Buffer + /// An internal buffer for parsing numbers from a string. private struct NumberBuffer { - private var buffer: String = "" + private var buffer = "" + + var isEmpty: Bool { buffer.isEmpty } mutating func append(_ scalar: UnicodeScalar) { - buffer.append(Character(scalar)) + buffer.append(String(scalar)) } mutating func flush() -> Int? { @@ -73,33 +109,40 @@ private struct NumberBuffer { } } +// MARK: - Tokenizer + /// Converts a dice-formatted string into a sequence of tokens. -/// If an unknown character is encountered, an empty array is returned. -internal func tokenize(_ string: String) throws -> [Token] { - var tokens = [Token]() +/// +/// - Parameter string: The string to tokenize (e.g., "2d6+3") +/// - Returns: An array of tokens representing the parsed string +/// - Throws: `DiceParseError.invalidCharacter` if an unknown character is encountered +func tokenize(_ string: String) throws -> [Token] { + var tokens: [Token] = [] var numberBuffer = NumberBuffer() for scalar in string.unicodeScalars { - // Numbers consume multiple characters if CharacterSet.decimalDigits.contains(scalar) { + // Numbers consume multiple characters numberBuffer.append(scalar) } else { - // Flush the current number before parsing the next character + // Flush any accumulated number before processing the next character if let value = numberBuffer.flush() { tokens.append(.number(value)) } - // Skip spaces and newlines + // Skip whitespace guard !CharacterSet.whitespacesAndNewlines.contains(scalar) else { continue } - if let token = Token(from: scalar) { - tokens.append(token) - } else { + // Parse token or throw error for invalid characters + guard let token = Token(from: scalar) else { throw DiceParseError.invalidCharacter(String(scalar)) } + + tokens.append(token) } } + // Flush any remaining number if let value = numberBuffer.flush() { tokens.append(.number(value)) } @@ -107,93 +150,119 @@ internal func tokenize(_ string: String) throws -> [Token] { return tokens } -/// Look-ahead at the next token and return whether it's `.drop`. -private func isDropping(_ tokens: [Token], after index: Int) -> Bool { - guard index + 1 < tokens.count else { return false } - let nextToken = tokens[index + 1] - return nextToken.isDropping -} +// MARK: - Parser State /// The internal state of the parser when it processes tokens. private struct DiceParserState { - var lastNumber: Int? = nil - var lastDice: Dice? = nil - var lastMathOperator: String? = nil - var isParsingDie = false + private(set) var lastNumber: Int? + private(set) var lastDice: Dice? + private(set) var lastMathOperator: String? + private(set) var isParsingDie = false + + // MARK: Parsing Methods - /// Parses a number and stores it either in lastDice sides or lastNumber + /// Parses a number and stores it either in `lastDice.sides` or `lastNumber`. + /// + /// - Parameter number: The number value to parse + /// - Throws: `DiceParseError` if consecutive numbers or dice expressions are encountered mutating func parse(number: Int) throws { if isParsingDie { - guard lastDice == nil else { throw DiceParseError.consecutiveDiceExpressions } - guard let die = Die(rawValue: number) else { throw DiceParseError.invalidDieSides(number) } + guard lastDice == nil else { + throw DiceParseError.consecutiveDiceExpressions + } + guard let die = Die(rawValue: number) else { + throw DiceParseError.invalidDieSides(number) + } + let times = lastNumber ?? 1 lastDice = SimpleDice(die, times: times) isParsingDie = false lastNumber = nil } else { - guard lastNumber == nil else { throw DiceParseError.consecutiveNumbers } + guard lastNumber == nil else { + throw DiceParseError.consecutiveNumbers + } lastNumber = number } } - /// Initiates parsing a die expression; finishes when parsing dice sides as an integer. + /// Initiates parsing a die expression. + /// The die is completed when parsing dice sides as an integer. + /// + /// - Throws: `DiceParseError.consecutiveDiceExpressions` if already parsing a die mutating func parseDie() throws { - guard !isParsingDie else { throw DiceParseError.consecutiveDiceExpressions } + guard !isParsingDie else { + throw DiceParseError.consecutiveDiceExpressions + } isParsingDie = true } - /// Parses a dropping dice supported by DroppingDice. - /// Must be preceded by a SimpleDice and a '-' math operator. + /// Parses a dropping dice modifier supported by `DroppingDice`. + /// Must be preceded by a `SimpleDice` and a '-' math operator. + /// + /// - Parameter drop: The drop modifier string ("L" or "H") + /// - Throws: `DiceParseError` if preconditions are not met mutating func parse(drop: String) throws { - guard let simpleDice = lastDice as? SimpleDice else { throw DiceParseError.missingSimpleDice } - guard lastMathOperator == "-" else { throw DiceParseError.missingMinus } + guard let simpleDice = lastDice as? SimpleDice else { + throw DiceParseError.missingSimpleDice + } + guard lastMathOperator == "-" else { + throw DiceParseError.missingMinus + } let diceDrop = DroppingDice.Drop(rawValue: drop)! lastDice = DroppingDice(simpleDice, drop: diceDrop) lastMathOperator = nil } - /// Parses a math operator supported by CompoundDice. + /// Parses a math operator supported by `CompoundDice`. + /// + /// - Parameter math: The math operator string + /// - Throws: `DiceParseError.consecutiveMathOperators` if another operator is pending mutating func parse(math: String) throws { - guard lastMathOperator == nil else { throw DiceParseError.consecutiveMathOperators } + guard lastMathOperator == nil else { + throw DiceParseError.consecutiveMathOperators + } lastMathOperator = math } - // Returns a Dice from either the last number (DiceModifier) or lastDice, and resets their state. + /// Returns a `Dice` from either the last number (`DiceModifier`) or `lastDice`, + /// and resets their state. + /// + /// - Returns: A `Dice` instance or `nil` if no pending dice exist mutating func flush() -> Dice? { - let returnDice: Dice? - if let number = lastNumber { - returnDice = DiceModifier(number) lastNumber = nil + return DiceModifier(number) } else if let dice = lastDice { - returnDice = dice lastDice = nil - } else { - returnDice = nil + return dice } - - return returnDice + return nil } - /// Returns combined dice from the current parsed dice passed in as lhs, - /// and the current parse state as rhs. + /// Returns combined dice from the current parsed dice and the current parse state. + /// + /// If there is no current parsed dice, the current parse state is returned. + /// If there is no math operator or no right-hand side, the left-hand side is returned. /// - /// If there is no current parsed dice, the current parse state is returned. - /// If there is no math operator or no rhs, lhs is returned. + /// - Parameter lhsDice: The left-hand side dice to combine with current state + /// - Returns: The combined dice or the original dice if no combination is possible mutating func combine(_ lhsDice: Dice?) -> Dice? { - guard let lhsDice = lhsDice else { return flush() } - guard let mathOperator = lastMathOperator, let rhsDice = flush() else { return lhsDice } + guard let lhsDice else { return flush() } + guard let mathOperator = lastMathOperator, let rhsDice = flush() else { + return lhsDice + } - // If we have a left hand side, a math operator and a right hand side, combine them. - let returnDice = CompoundDice(lhs: lhsDice, rhs: rhsDice, mathOperator: mathOperator) + // Combine left-hand side, math operator, and right-hand side lastMathOperator = nil - - return returnDice + return CompoundDice(lhs: lhsDice, rhs: rhsDice, mathOperator: mathOperator) } - // Checks for invalid or incomplete state at the end of parsing. - func finishParsing() throws { + /// Checks for invalid or incomplete state at the end of parsing. + /// + /// - Throws: `DiceParseError` if the parser is in an incomplete state + func validate() throws { if isParsingDie { throw DiceParseError.missingDieSides } else if lastMathOperator != nil { @@ -202,101 +271,131 @@ private struct DiceParserState { } } -/// Converts an array of tokens into Dice. -internal func parse(_ tokens: [Token]) throws -> Dice? { - var parsedDice: Dice? = nil - +// MARK: - Parser + +/// Look-ahead at the next token and return whether it's a `.drop` token. +private func isNextTokenDropping(_ tokens: [Token], after index: Int) -> Bool { + guard index + 1 < tokens.count else { return false } + return tokens[index + 1].isDropping +} + +/// Converts an array of tokens into a `Dice` object. +/// +/// - Parameter tokens: The tokens to parse +/// - Returns: A `Dice` instance representing the parsed expression, or `nil` if empty +/// - Throws: `DiceParseError` if the token sequence is invalid +func parse(_ tokens: [Token]) throws -> Dice? { + var parsedDice: Dice? var state = DiceParserState() + for (index, token) in tokens.enumerated() { switch token { case .number(let value): try state.parse(number: value) + case .die: try state.parseDie() + case .drop(let drop): try state.parse(drop: drop) + case .mathOperator(let math): - if !isDropping(tokens, after: index) { + // Only combine if the next token isn't a drop modifier + if !isNextTokenDropping(tokens, after: index) { parsedDice = state.combine(parsedDice) } try state.parse(math: math) } } + parsedDice = state.combine(parsedDice) - try state.finishParsing() + try state.validate() return parsedDice } +// MARK: - String Extension + public extension String { - /// Creates a Dice instance from a string formatted as: - /// > `[]d[|-]*` - /// - Supported dice sides are 4, 6, 8, 10, 12, 20 and %. - /// - times, modifier and dropping (-L for lowest, -H for highest) are optional. + /// Creates a `Dice` instance from a dice notation string. /// - /// - parameter from: A string, for example: "d8", "2d12+2", "4d6-L", "1", "2d4+3d12-4". - /// - returns: Dice representing the parsed string. Returns `nil` if the string - /// could not be interpreted; for example, if there are extraneous - /// characters, or an unsupported dice such as d7 is specified. + /// Supported format: `[]d[|-]*` + /// + /// Examples: + /// - `"d8"` → Simple 8-sided die + /// - `"2d12+2"` → Two 12-sided dice plus 2 + /// - `"4d6-L"` → Four 6-sided dice, drop lowest + /// - `"1"` → Constant modifier of 1 + /// - `"2d4+3d12-4"` → Compound expression + /// + /// Supported dice sides: 4, 6, 8, 10, 12, 20, and % (100) + /// + /// - Returns: A `Dice` instance, or `nil` if the string cannot be parsed var parseDice: Dice? { - var dice: Dice? = nil - do { let tokens = try tokenize(self) - dice = try parse(tokens) - } - catch let error { + return try parse(tokens) + } catch { print("Error parsing dice: \(error.localizedDescription)") + return nil } - - return dice } } -// TODO: this works around the fact that we can't explicitly make the Dice protocol Decodable. -// We first decode to String or Int, then create a derived Dice type from those. -// -// Containers can still match to the type (note Dice.protocol in the function declarations). -public extension KeyedDecodingContainer { +// MARK: - Decoding Extensions + +public extension KeyedDecodingContainer { - /// Decodes either an integer or a formatted string into a Dice. - /// See `String.parseDice` for supported string formats. + /// Decodes either an integer or a dice notation string into a `Dice`. /// - /// - throws `DecodingError.dataCorrupted` if the dice is not present or could not be decoded. + /// - Parameters: + /// - type: The `Dice.Protocol` metatype + /// - key: The coding key for the value + /// - Returns: A decoded `Dice` instance + /// - Throws: `DecodingError.dataCorrupted` if the value cannot be decoded as dice func decode(_ type: Dice.Protocol, forKey key: K) throws -> Dice { - let dice: Dice? - - if let number = try? self.decode(Int.self, forKey: key) { - dice = DiceModifier(number) - } else { - dice = try self.decode(String.self, forKey: key).parseDice + // Try decoding as an integer first (for constant modifiers) + if let number = try? decode(Int.self, forKey: key) { + return DiceModifier(number) } - // Throw if we were unsuccessful parsing. - guard dice != nil else { - let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Missing string or number for Dice value") - throw DecodingError.dataCorrupted(context) + // Try decoding as a string and parsing as dice notation + if let string = try? decode(String.self, forKey: key), + let dice = string.parseDice { + return dice } - return dice! + // Throw if neither approach succeeded + let context = DecodingError.Context( + codingPath: codingPath, + debugDescription: "Could not decode Dice from string or number" + ) + throw DecodingError.dataCorrupted(context) } - /// Decodes either an integer or a formatted string into a Dice, if present. - /// See `String.parseDice` for supported string formats. + /// Decodes either an integer or a dice notation string into a `Dice`, if present. /// - /// - throws `DecodingError.dataCorrupted` if the dice could not be decoded. + /// - Parameters: + /// - type: The `Dice.Protocol` metatype + /// - key: The coding key for the value + /// - Returns: A decoded `Dice` instance, or `nil` if the key is not present + /// - Throws: `DecodingError.dataCorrupted` if the value is present but cannot be decoded func decodeIfPresent(_ type: Dice.Protocol, forKey key: K) throws -> Dice? { - let dice: Dice? + // Return nil if the key doesn't exist + guard contains(key) else { return nil } - if let number = try? self.decode(Int.self, forKey: key) { - dice = DiceModifier(number) - } else if let string = try self.decodeIfPresent(String.self, forKey: key) { - dice = string.parseDice - } else { - dice = nil + // Try decoding as an integer first + if let number = try? decode(Int.self, forKey: key) { + return DiceModifier(number) } - return dice + // Try decoding as a string and parsing as dice notation + if let string = try? decode(String.self, forKey: key) { + return string.parseDice + } + + return nil } } + From 318c003c8125f28e3e7a8cdfb3e7dec670fa6878 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Fri, 7 Nov 2025 09:47:21 -0500 Subject: [PATCH 19/33] Used Claude to improve the Dice implementation. --- .../RolePlayingCore/Dice/CompoundDice.swift | 11 +++++--- .../RolePlayingCore/Dice/DiceModifier.swift | 3 +++ Sources/RolePlayingCore/Dice/Die.swift | 7 +----- .../RolePlayingCore/Dice/DroppingDice.swift | 14 +++++++---- Sources/RolePlayingCore/Dice/SimpleDice.swift | 25 +++++++------------ 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Sources/RolePlayingCore/Dice/CompoundDice.swift b/Sources/RolePlayingCore/Dice/CompoundDice.swift index d428da7..857158b 100644 --- a/Sources/RolePlayingCore/Dice/CompoundDice.swift +++ b/Sources/RolePlayingCore/Dice/CompoundDice.swift @@ -41,13 +41,18 @@ public struct CompoundDice: Dice { /// Mapping of strings to function signatures. internal let mathOperators: [String: MathOperator] = ["+": (+), "-": (-), "x": (*), "*": (*), "/": (/)] - /// Rolls the specified number of times, optionally adding or multiplying a modifier, - /// and returning the result. + /// Rolls the dice on both sides and combines them with the math operator, + /// returning the result. public func roll() -> DiceRoll { let lhsRoll = lhs.roll() let rhsRoll = rhs.roll() - let result = mathOperators[mathOperator]!(lhsRoll.result, rhsRoll.result) + guard let operation = mathOperators[mathOperator] else { + // Fallback if operator is unknown (shouldn't happen if properly constructed) + return DiceRoll(lhsRoll.result, "\(lhsRoll.description) ? \(rhsRoll.description)") + } + + let result = operation(lhsRoll.result, rhsRoll.result) let description = "\(lhsRoll.description) \(mathOperator) \(rhsRoll.description)" return DiceRoll(result, description) diff --git a/Sources/RolePlayingCore/Dice/DiceModifier.swift b/Sources/RolePlayingCore/Dice/DiceModifier.swift index bc8266b..000b348 100644 --- a/Sources/RolePlayingCore/Dice/DiceModifier.swift +++ b/Sources/RolePlayingCore/Dice/DiceModifier.swift @@ -19,6 +19,9 @@ public struct DiceModifier: Dice { return DiceRoll(modifier, "\(modifier)") } + /// Returns the modifier value to conform to the Dice protocol. + /// Note: A modifier doesn't have "sides" in the traditional sense, + /// but this property is required by the protocol. public var sides: Int { modifier } public var description: String { "\(modifier)" } diff --git a/Sources/RolePlayingCore/Dice/Die.swift b/Sources/RolePlayingCore/Dice/Die.swift index 3c43856..80436a0 100644 --- a/Sources/RolePlayingCore/Dice/Die.swift +++ b/Sources/RolePlayingCore/Dice/Die.swift @@ -30,12 +30,7 @@ public enum Die: Int { /// Rolls the specified number of times and returns an array of numbers between 1 and this dice type. public func roll(_ times: Int) -> [Int] { - var rolls = [Int](repeating: 0, count: times) // preallocating is much faster than appending - - for index in 0 ..< times { - rolls[index] = roll() - } - return rolls + return (0.. DiceRoll { - let lastRoll: [Int] = dice.roll() - let droppedRoll = (drop == .lowest) ? lastRoll.min() : lastRoll.max() + let lastRoll = dice.rollAll() - let result = lastRoll.reduce(0, +) - droppedRoll! - let description = "(\(dice.rollDescription(lastRoll)) - \(droppedRoll!))" + guard let droppedRoll = drop == .lowest ? lastRoll.min() : lastRoll.max() else { + // Edge case: no rolls to drop (shouldn't happen in practice) + return DiceRoll(0, "(0)") + } + + let result = lastRoll.reduce(0, +) - droppedRoll + let description = "(\(dice.rollDescription(lastRoll)) - \(droppedRoll))" return DiceRoll(result, description) } diff --git a/Sources/RolePlayingCore/Dice/SimpleDice.swift b/Sources/RolePlayingCore/Dice/SimpleDice.swift index 1271f25..04bd19c 100644 --- a/Sources/RolePlayingCore/Dice/SimpleDice.swift +++ b/Sources/RolePlayingCore/Dice/SimpleDice.swift @@ -20,17 +20,15 @@ public struct SimpleDice: Dice { } /// Rolls the specified number of times, returning the array of rolls. - internal func roll() -> [Int] { + internal func rollAll() -> [Int] { return die.roll(times) } /// Rolls the specified number of times, returning the sum of the rolls and a description. public func roll() -> DiceRoll { - let lastRoll: [Int] = roll() - + let lastRoll = rollAll() let result = lastRoll.reduce(0, +) let description = rollDescription(lastRoll) - return DiceRoll(result, description) } @@ -45,20 +43,15 @@ public struct SimpleDice: Dice { /// Returns the last roll as a sequence of added numbers in parenthesis. internal func rollDescription(_ lastRoll: [Int]) -> String { - var resultString: String + guard !lastRoll.isEmpty else { return "0" } - var rolls = lastRoll - let last = rolls.popLast()! - if rolls.count == 0 { - resultString = "\(last)" - } else { - resultString = "(" - for roll in rolls { - resultString += "\(roll) + " - } - resultString += "\(last))" + // Single roll doesn't need parentheses + guard lastRoll.count > 1 else { + return "\(lastRoll[0])" } - return resultString + // Multiple rolls: format as (roll1 + roll2 + ... + rollN) + let rollsString = lastRoll.map(String.init).joined(separator: " + ") + return "(\(rollsString))" } } From f56d03c0066298d08f852c1a18e6b5e663f10ed4 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Fri, 7 Nov 2025 09:53:15 -0500 Subject: [PATCH 20/33] Minor adjustments to comments. --- Sources/RolePlayingCore/Dice/CompoundDice.swift | 4 ++-- Sources/RolePlayingCore/Dice/DiceRoll.swift | 2 +- Sources/RolePlayingCore/Dice/Die.swift | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/RolePlayingCore/Dice/CompoundDice.swift b/Sources/RolePlayingCore/Dice/CompoundDice.swift index 857158b..49df869 100644 --- a/Sources/RolePlayingCore/Dice/CompoundDice.swift +++ b/Sources/RolePlayingCore/Dice/CompoundDice.swift @@ -12,8 +12,8 @@ import Foundation /// General-purpose composition of dice rolls. /// /// The two primary use cases for this type are: -/// - combining two rolls, e.g., "2d4+d6", -/// - using a modifier, e.g., "d12+2". +/// - combining two rolls, e.g., "`2d4+d6`", +/// - using a modifier, e.g., "`d12+2`". public struct CompoundDice: Dice { public let lhs: Dice public let rhs: Dice diff --git a/Sources/RolePlayingCore/Dice/DiceRoll.swift b/Sources/RolePlayingCore/Dice/DiceRoll.swift index 221bb67..c65146b 100644 --- a/Sources/RolePlayingCore/Dice/DiceRoll.swift +++ b/Sources/RolePlayingCore/Dice/DiceRoll.swift @@ -13,7 +13,7 @@ public struct DiceRoll: CustomStringConvertible { public let result: Int /// A string representing the intermediate values of the dice roll. - /// For example, a "3d6" might return "(4+1+5)". + /// For example, a "`3d6`" might return "`(4+1+5)`". public let description: String /// Creates a roll with its accompanying description of intermediate values. diff --git a/Sources/RolePlayingCore/Dice/Die.swift b/Sources/RolePlayingCore/Dice/Die.swift index 80436a0..4f99824 100644 --- a/Sources/RolePlayingCore/Dice/Die.swift +++ b/Sources/RolePlayingCore/Dice/Die.swift @@ -15,7 +15,8 @@ public enum Die: Int { case d10 = 10 case d12 = 12 case d20 = 20 - case d100 = 100 // AKA "d%" + /// Also known as "`d%`" + case d100 = 100 /// Rolls once and returns a number between 1 and this dice type. public func roll(using generator: inout G) -> Int { From cdd107085dafafe50c4ce4f54729b1ba21bd0e16 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Fri, 7 Nov 2025 10:28:07 -0500 Subject: [PATCH 21/33] Eliminated potential race conditions with currencies. --- .../RolePlayingCore/Currency/Currencies.swift | 35 ++++++++++++++----- Sources/RolePlayingCore/Currency/Money.swift | 4 ++- .../RolePlayingCoreTests/CurrencyTests.swift | 4 +-- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/Sources/RolePlayingCore/Currency/Currencies.swift b/Sources/RolePlayingCore/Currency/Currencies.swift index f6d87c3..f64b57b 100644 --- a/Sources/RolePlayingCore/Currency/Currencies.swift +++ b/Sources/RolePlayingCore/Currency/Currencies.swift @@ -12,10 +12,10 @@ import Foundation public struct Currencies { /// A map of all currently loaded currencies. - internal private(set) static nonisolated(unsafe) var allCurrencies: [String: UnitCurrency] = [:] + private static nonisolated(unsafe) var allCurrencies: [String: UnitCurrency] = [:] /// The default base unit is a currency called "credit". It may be replaced at runtime. - fileprivate static nonisolated(unsafe) var baseUnitCurrency = UnitCurrency(symbol: "c", converter: UnitConverterLinear(coefficient: 1.0), name: "credit", plural: "credits") + private static nonisolated(unsafe) var baseUnitCurrency = UnitCurrency(symbol: "c", converter: UnitConverterLinear(coefficient: 1.0), name: "credit", plural: "credits") /// A lock to protect access to allCurrencies from multiple threads. private static let lock = NSLock() @@ -28,7 +28,7 @@ public struct Currencies { } /// Adds the unit currency to the collection of currencies. - public static func add(_ currency: UnitCurrency) { + fileprivate static func add(_ currency: UnitCurrency) { lock.lock() defer { lock.unlock() } allCurrencies[currency.symbol] = currency @@ -55,6 +55,21 @@ public struct Currencies { defer { lock.unlock() } return baseUnitCurrency } + + /// Returns a snapshot of all currency values as an array, and the base currency. + fileprivate static func allCurrenciesAndBase() -> (all: [UnitCurrency], base: UnitCurrency) { + lock.lock() + defer { lock.unlock() } + return (Array(allCurrencies.values), baseUnitCurrency) + } + + /// Returns a snapshot of all currency values as an array (for safe iteration). + /// - Returns: Array copy of all currencies to avoid holding the lock during iteration + internal static func allValues() -> [UnitCurrency] { + lock.lock() + defer { lock.unlock() } + return Array(allCurrencies.values) + } } extension Currencies: Codable { @@ -77,12 +92,12 @@ extension Currencies: Codable { } // For writing - init(_ unitCurrency: UnitCurrency) { + init(_ unitCurrency: UnitCurrency, isDefault: Bool) { self.symbol = unitCurrency.symbol self.coefficient = (unitCurrency.converter as! UnitConverterLinear).coefficient self.name = unitCurrency.name self.plural = unitCurrency.plural - self.isDefault = unitCurrency == .baseUnit() + self.isDefault = isDefault } // For reading @@ -122,13 +137,15 @@ extension Currencies: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - Currencies.lock.lock() - let allCurrenciesSnapshot = Currencies.allCurrencies.values - Currencies.lock.unlock() + // Take a snapshot of all currencies AND the base unit in a single lock + let allCurrenciesSnapshot = Currencies.allValues() + let baseUnit = Currencies.base() + // Now safely process the snapshot without holding the lock var currencies = [Currency]() for unitCurrency in allCurrenciesSnapshot { - let currency = Currency(unitCurrency) + let isDefault = unitCurrency == baseUnit + let currency = Currency(unitCurrency, isDefault: isDefault) currencies.append(currency) } diff --git a/Sources/RolePlayingCore/Currency/Money.swift b/Sources/RolePlayingCore/Currency/Money.swift index 8993e9b..f7d940e 100644 --- a/Sources/RolePlayingCore/Currency/Money.swift +++ b/Sources/RolePlayingCore/Currency/Money.swift @@ -19,7 +19,9 @@ public extension String { var value: Double? var unit: UnitCurrency = .baseUnit() - for currency in Currencies.allCurrencies.values { + // Get a thread-safe snapshot of all currencies + let allCurrencies = Currencies.allValues() + for currency in allCurrencies { if let range = self.range(of: currency.symbol), range.upperBound == self.endIndex { value = Double(self[.. Date: Sat, 8 Nov 2025 09:15:28 -0500 Subject: [PATCH 22/33] Updated to use a wrapper Storage, and refactored to improve separation of concerns. --- .../RolePlayingCore/Currency/Currencies.swift | 164 ++++++------------ Sources/RolePlayingCore/Currency/Money.swift | 2 +- .../Currency/UnitCurrency.swift | 46 ++++- .../RolePlayingCoreTests/CurrencyTests.swift | 4 +- 4 files changed, 97 insertions(+), 119 deletions(-) diff --git a/Sources/RolePlayingCore/Currency/Currencies.swift b/Sources/RolePlayingCore/Currency/Currencies.swift index f64b57b..d5167db 100644 --- a/Sources/RolePlayingCore/Currency/Currencies.swift +++ b/Sources/RolePlayingCore/Currency/Currencies.swift @@ -11,107 +11,65 @@ import Foundation /// A collection of currencies. public struct Currencies { - /// A map of all currently loaded currencies. - private static nonisolated(unsafe) var allCurrencies: [String: UnitCurrency] = [:] - - /// The default base unit is a currency called "credit". It may be replaced at runtime. - private static nonisolated(unsafe) var baseUnitCurrency = UnitCurrency(symbol: "c", converter: UnitConverterLinear(coefficient: 1.0), name: "credit", plural: "credits") - - /// A lock to protect access to allCurrencies from multiple threads. - private static let lock = NSLock() - - /// Returns the unit currency corresponding to this symbol. Returns nil if no symbol matches. - public static func find(_ symbol: String) -> UnitCurrency? { - lock.lock() - defer { lock.unlock() } - return Currencies.allCurrencies[symbol] - } - - /// Adds the unit currency to the collection of currencies. - fileprivate static func add(_ currency: UnitCurrency) { - lock.lock() - defer { lock.unlock() } - allCurrencies[currency.symbol] = currency - } - - /// Makes this unit currency the default for `Money`. - fileprivate static func setDefault(_ newBaseUnit: UnitCurrency) { - lock.lock() - defer { lock.unlock() } + /// Thread-safe storage using NSLock for synchronization. + private final class Storage: @unchecked Sendable { + private let lock = NSLock() + + /// A map of all currently loaded currencies. + private var allCurrencies: [String: UnitCurrency] = [:] - // Remove the old base unit from all currencies. - let oldSymbol = baseUnitCurrency.symbol - guard oldSymbol != newBaseUnit.symbol else { - return + /// The default or base unit currency. It must be set at runtime. + private var baseUnitCurrency: UnitCurrency! + + func add(_ currencies: [UnitCurrency]) { + lock.lock() + defer { lock.unlock() } + + for currency in currencies { + allCurrencies[currency.symbol] = currency + if currency.isDefault { + baseUnitCurrency = currency + } + } } - allCurrencies[oldSymbol] = nil + func find(_ symbol: String) -> UnitCurrency? { + lock.lock() + defer { lock.unlock() } + return allCurrencies[symbol] + } - baseUnitCurrency = newBaseUnit + var base: UnitCurrency { + lock.lock() + defer { lock.unlock() } + return baseUnitCurrency + } + + var all: [UnitCurrency] { + lock.lock() + defer { lock.unlock() } + return Array(allCurrencies.values) + } } - internal static func base() -> UnitCurrency { - lock.lock() - defer { lock.unlock() } - return baseUnitCurrency - } + /// Shared storage instance. + private static let storage = Storage() - /// Returns a snapshot of all currency values as an array, and the base currency. - fileprivate static func allCurrenciesAndBase() -> (all: [UnitCurrency], base: UnitCurrency) { - lock.lock() - defer { lock.unlock() } - return (Array(allCurrencies.values), baseUnitCurrency) - } + /// Returns the unit currency corresponding to this symbol. Returns nil if no symbol matches. + public static func find(_ symbol: String) -> UnitCurrency? { storage.find(symbol) } - /// Returns a snapshot of all currency values as an array (for safe iteration). - /// - Returns: Array copy of all currencies to avoid holding the lock during iteration - internal static func allValues() -> [UnitCurrency] { - lock.lock() - defer { lock.unlock() } - return Array(allCurrencies.values) - } + /// Adds a collection of currencies, and updates the default or base unit currency. + fileprivate static func add(_ currencies: [UnitCurrency]) { storage.add(currencies) } + + /// Returns the base unit currency. + public static var base: UnitCurrency { storage.base } + + /// Returns all of the currently loaded currencies. + public static var all: [UnitCurrency] { storage.all } } extension Currencies: Codable { - /// TODO: UnitCurrency's Dimension conforms to NSCoding, not Codable . To support Codable, we - /// use this type to mirror UnitCurrency, and then map it once decoded. - private struct Currency: Codable { - let symbol: String - let coefficient: Double - let name: String - let plural: String - let isDefault: Bool - - private enum CodingKeys: String, CodingKey { - case symbol - case coefficient - case name - case plural - case isDefault = "is default" - } - - // For writing - init(_ unitCurrency: UnitCurrency, isDefault: Bool) { - self.symbol = unitCurrency.symbol - self.coefficient = (unitCurrency.converter as! UnitConverterLinear).coefficient - self.name = unitCurrency.name - self.plural = unitCurrency.plural - self.isDefault = isDefault - } - - // For reading - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - symbol = try container.decode(String.self, forKey: .symbol) - coefficient = try container.decode(Double.self, forKey: .coefficient) - name = try container.decode(String.self, forKey: .name) - plural = try container.decode(String.self, forKey: .plural) - isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault) ?? false - } - } - private enum CodingKeys: String, CodingKey { case currencies } @@ -120,35 +78,15 @@ extension Currencies: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let currencies = try container.decode([Currency].self, forKey: .currencies) - for currency in currencies { - let converter = UnitConverterLinear(coefficient: currency.coefficient) - let unitCurrency = UnitCurrency(symbol: currency.symbol, converter: converter, name: currency.name, plural: currency.plural) - - Currencies.add(unitCurrency) - - if currency.isDefault { - Currencies.setDefault(unitCurrency) - } - } + let currencies = try container.decode([UnitCurrency].self, forKey: .currencies) + Currencies.add(currencies) } /// Encodes an array of currencies. public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - // Take a snapshot of all currencies AND the base unit in a single lock - let allCurrenciesSnapshot = Currencies.allValues() - let baseUnit = Currencies.base() - - // Now safely process the snapshot without holding the lock - var currencies = [Currency]() - for unitCurrency in allCurrenciesSnapshot { - let isDefault = unitCurrency == baseUnit - let currency = Currency(unitCurrency, isDefault: isDefault) - currencies.append(currency) - } - - try container.encode(currencies, forKey: .currencies) + let allCurrencies = Currencies.all + try container.encode(allCurrencies, forKey: .currencies) } } diff --git a/Sources/RolePlayingCore/Currency/Money.swift b/Sources/RolePlayingCore/Currency/Money.swift index f7d940e..2971914 100644 --- a/Sources/RolePlayingCore/Currency/Money.swift +++ b/Sources/RolePlayingCore/Currency/Money.swift @@ -20,7 +20,7 @@ public extension String { var unit: UnitCurrency = .baseUnit() // Get a thread-safe snapshot of all currencies - let allCurrencies = Currencies.allValues() + let allCurrencies = Currencies.all for currency in allCurrencies { if let range = self.range(of: currency.symbol), range.upperBound == self.endIndex { value = Double(self[.. UnitCurrency { - return Currencies.base() + return Currencies.base } - public init(symbol: String, converter: UnitConverter, name: String, plural: String) { + public init(symbol: String, converter: UnitConverter, name: String, plural: String, isDefault: Bool = false) { self.name = name self.plural = plural + self.isDefault = isDefault super.init(symbol: symbol, converter: converter) } @@ -35,3 +38,40 @@ public final class UnitCurrency : Dimension, @unchecked Sendable { super.init(coder: aDecoder) } } + +// MARK: Codable support + +extension UnitCurrency: Codable { + + private enum CodingKeys: String, CodingKey { + case symbol + case coefficient + case name + case plural + case isDefault = "is default" + } + + public convenience init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let symbol = try container.decode(String.self, forKey: .symbol) + let coefficient = try container.decode(Double.self, forKey: .coefficient) + let name = try container.decode(String.self, forKey: .name) + let plural = try container.decode(String.self, forKey: .plural) + let isDefault = try container.decodeIfPresent(Bool.self, forKey: .isDefault) ?? false + + let converter = UnitConverterLinear(coefficient: coefficient) + self.init(symbol: symbol, converter: converter, name: name, plural: plural, isDefault: isDefault) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.symbol, forKey: .symbol) + try container.encode(self.converter.baseUnitValue(fromValue: 1.0), forKey: .coefficient) + try container.encode(self.name, forKey: .name) + try container.encode(self.plural, forKey: .plural) + if self.isDefault { + try container.encode(self.isDefault, forKey: .isDefault) + } + } +} diff --git a/Tests/RolePlayingCoreTests/CurrencyTests.swift b/Tests/RolePlayingCoreTests/CurrencyTests.swift index 92128c3..dddc7cd 100644 --- a/Tests/RolePlayingCoreTests/CurrencyTests.swift +++ b/Tests/RolePlayingCoreTests/CurrencyTests.swift @@ -110,12 +110,12 @@ struct UnitCurrencyTests { @Test("Duplicate currencies are ignored") func duplicateCurrencies() async throws { - #expect(Currencies.allValues().count == 5, "currencies count") + #expect(Currencies.all.count == 5, "currencies count") let data = try bundle.loadJSON("TestCurrencies") _ = try decoder.decode(Currencies.self, from: data) - #expect(Currencies.allValues().count == 5, "currencies count should remain 5") + #expect(Currencies.all.count == 5, "currencies count should remain 5") } @Test("Missing currency traits") From 2cc3ce47c1ffe17b1321a70c3cf378d252e3ca67 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sat, 8 Nov 2025 15:00:49 -0500 Subject: [PATCH 23/33] Removed global state from Currencies, and added a Mutex to base unit currency. Converted Money and Player to CodableWithConfiguration so that currencies can be passed down, in lieu of a global. --- Package.swift | 4 +- .../Configuration/Configuration.swift | 9 ++- .../RolePlayingCore/Currency/Currencies.swift | 78 ++++++------------- Sources/RolePlayingCore/Currency/Money.swift | 73 +++++++---------- .../Currency/UnitCurrency.swift | 13 +++- Sources/RolePlayingCore/Player/Player.swift | 10 +-- Sources/RolePlayingCore/Player/Players.swift | 24 +++++- .../RolePlayingCoreTests/CurrencyTests.swift | 73 +++++++++-------- Tests/RolePlayingCoreTests/PlayerTests.swift | 23 +++--- Tests/RolePlayingCoreTests/PlayersTests.swift | 29 ++----- 10 files changed, 155 insertions(+), 181 deletions(-) diff --git a/Package.swift b/Package.swift index 0269648..c68b2a1 100644 --- a/Package.swift +++ b/Package.swift @@ -6,8 +6,8 @@ import PackageDescription let package = Package( name: "RolePlayingCore", platforms: [ - .iOS(.v17), - .macOS(.v14) + .iOS(.v18), + .macOS(.v15) ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. diff --git a/Sources/RolePlayingCore/Configuration/Configuration.swift b/Sources/RolePlayingCore/Configuration/Configuration.swift index acd76fa..41f5c06 100644 --- a/Sources/RolePlayingCore/Configuration/Configuration.swift +++ b/Sources/RolePlayingCore/Configuration/Configuration.swift @@ -36,6 +36,7 @@ public struct Configuration { public var configurationFiles: ConfigurationFiles + public var currencies = Currencies() public var backgrounds = Backgrounds() public var skills = Skills() public var species = Species() @@ -55,12 +56,14 @@ public struct Configuration { for currenciesFile in configurationFiles.currencies { let jsonData = try bundle.loadJSON(currenciesFile) - _ = try jsonDecoder.decode(Currencies.self, from: jsonData) + let currencies = try jsonDecoder.decode(Currencies.self, from: jsonData) + self.currencies.add(currencies.all) } for skillsFile in configurationFiles.skills { let jsonData = try bundle.loadJSON(skillsFile) - _ = try jsonDecoder.decode(Skills.self, from: jsonData) + let skills = try jsonDecoder.decode(Skills.self, from: jsonData) + self.skills.skills += skills.skills } for backgroundsFile in configurationFiles.backgrounds { @@ -93,7 +96,7 @@ public struct Configuration { if let playersFiles = configurationFiles.players { for playersFile in playersFiles { let jsonData = try bundle.loadJSON(playersFile) - let players = try jsonDecoder.decode(Players.self, from: jsonData) + let players = try jsonDecoder.decode(Players.self, from: jsonData, configuration: self) try players.resolve(backgrounds: self.backgrounds, classes: self.classes, species: self.species) self.players.players += players.players } diff --git a/Sources/RolePlayingCore/Currency/Currencies.swift b/Sources/RolePlayingCore/Currency/Currencies.swift index d5167db..b8b54e2 100644 --- a/Sources/RolePlayingCore/Currency/Currencies.swift +++ b/Sources/RolePlayingCore/Currency/Currencies.swift @@ -10,62 +10,31 @@ import Foundation /// A collection of currencies. public struct Currencies { - - /// Thread-safe storage using NSLock for synchronization. - private final class Storage: @unchecked Sendable { - private let lock = NSLock() - - /// A map of all currently loaded currencies. - private var allCurrencies: [String: UnitCurrency] = [:] - - /// The default or base unit currency. It must be set at runtime. - private var baseUnitCurrency: UnitCurrency! - - func add(_ currencies: [UnitCurrency]) { - lock.lock() - defer { lock.unlock() } - for currency in currencies { - allCurrencies[currency.symbol] = currency - if currency.isDefault { - baseUnitCurrency = currency - } - } - } - - func find(_ symbol: String) -> UnitCurrency? { - lock.lock() - defer { lock.unlock() } - return allCurrencies[symbol] - } - - var base: UnitCurrency { - lock.lock() - defer { lock.unlock() } - return baseUnitCurrency - } - - var all: [UnitCurrency] { - lock.lock() - defer { lock.unlock() } - return Array(allCurrencies.values) + /// A dictionary of all currently loaded currencies. + 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] = []) { + 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. + 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) } } - /// Shared storage instance. - private static let storage = Storage() - - /// Returns the unit currency corresponding to this symbol. Returns nil if no symbol matches. - public static func find(_ symbol: String) -> UnitCurrency? { storage.find(symbol) } - - /// Adds a collection of currencies, and updates the default or base unit currency. - fileprivate static func add(_ currencies: [UnitCurrency]) { storage.add(currencies) } - - /// Returns the base unit currency. - public static var base: UnitCurrency { storage.base } - - /// Returns all of the currently loaded currencies. - public static var all: [UnitCurrency] { storage.all } + /// Returns a read-only array of all currencies. + var all: [UnitCurrency] { Array(allCurrencies.values) } } extension Currencies: Codable { @@ -79,14 +48,13 @@ extension Currencies: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) let currencies = try container.decode([UnitCurrency].self, forKey: .currencies) - Currencies.add(currencies) + add(currencies) } /// Encodes an array of currencies. public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - let allCurrencies = Currencies.all - try container.encode(allCurrencies, forKey: .currencies) + try container.encode(all, forKey: .currencies) } } diff --git a/Sources/RolePlayingCore/Currency/Money.swift b/Sources/RolePlayingCore/Currency/Money.swift index 2971914..0261894 100644 --- a/Sources/RolePlayingCore/Currency/Money.swift +++ b/Sources/RolePlayingCore/Currency/Money.swift @@ -11,16 +11,41 @@ import Foundation /// A measurement of currency. public typealias Money = Measurement +// NOTE: @retroactive is a compiler-suggested workaround in case Measurement +// adopts CodableWithConfiguration in the future. +extension Money: @retroactive CodableWithConfiguration { + + public init(from decoder: any Decoder, configuration: Currencies) throws { + let container = try decoder.singleValueContainer() + + if let double = try? container.decode(Double.self) { + self = Money(value: double, unit: UnitCurrency.baseUnit()) + } else { + let string = try container.decode(String.self) + if let money = string.parseMoney(configuration) { + self = money + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Failed to decode Money from \"\(string)\"") + } + } + } + + public func encode(to encoder: any Encoder, configuration: Currencies) throws { + var container = encoder.singleValueContainer() + try container.encode(self.description) + } +} + public extension String { /// Parses numbers with currency symbols into money. /// If there is no currency symbol, the number is associated with the base unit currency. - var parseMoney: Money? { + func parseMoney(_ configuration: Currencies) -> Money? { var value: Double? var unit: UnitCurrency = .baseUnit() // Get a thread-safe snapshot of all currencies - let allCurrencies = Currencies.all + let allCurrencies = configuration.all for currency in allCurrencies { if let range = self.range(of: currency.symbol), range.upperBound == self.endIndex { value = Double(self[.. Money { - let money: Money? - - if let double = try? self.decode(Double.self, forKey: key) { - money = Money(value: double, unit: .baseUnit()) - } else { - money = try self.decode(String.self, forKey: key).parseMoney - } - - // Throw if we were unsuccessful parsing. - guard money != nil else { - let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "Missing string or number for Money value") - throw DecodingError.dataCorrupted(context) - } - - return money! - } - - /// Decodes either a number or a string into Money, if present. - /// - /// - throws `DecodingError.dataCorrupted` if the money could not be decoded. - func decodeIfPresent(_ type: Money.Type, forKey key: K) throws -> Money? { - let money: Money? - - if let double = try? self.decode(Double.self, forKey: key) { - money = Money(value: double, unit: .baseUnit()) - } else if let string = try self.decodeIfPresent(String.self, forKey: key) { - money = string.parseMoney - } else { - money = nil - } - - return money - } - -} - - extension MeasurementFormatter { /// The formatter requires a specialization that knows how to find UnitCurrency's baseUnit() and diff --git a/Sources/RolePlayingCore/Currency/UnitCurrency.swift b/Sources/RolePlayingCore/Currency/UnitCurrency.swift index 7a2796c..10ab765 100644 --- a/Sources/RolePlayingCore/Currency/UnitCurrency.swift +++ b/Sources/RolePlayingCore/Currency/UnitCurrency.swift @@ -7,6 +7,7 @@ // import Foundation +import Synchronization /// Units of currency or coinage. /// @@ -19,10 +20,20 @@ public final class UnitCurrency : Dimension, @unchecked Sendable { /// The plural unit name used when the unitStyle is long. public internal(set) var plural: String! + /// Whether this currency is meant to be the default or base unit currency. public internal(set) var isDefault: Bool = false + /// The default or base unit currency. + private static let base: Mutex = Mutex(UnitCurrency(symbol: "Credits", converter: UnitConverterLinear(coefficient: 1.0), name: "Credit", plural: "Credits")) + + /// Sets the base unit currency using a Mutex to manage concurrency. + public static func setBaseUnit(_ unit: UnitCurrency) { + base.withLock { $0 = unit } + } + + /// Returns the base unit currency using a Mutex to manage concurrency. public override class func baseUnit() -> UnitCurrency { - return Currencies.base + base.withLock { return $0 } } public init(symbol: String, converter: UnitConverter, name: String, plural: String, isDefault: Bool = false) { diff --git a/Sources/RolePlayingCore/Player/Player.swift b/Sources/RolePlayingCore/Player/Player.swift index 2e32cfd..51e7179 100644 --- a/Sources/RolePlayingCore/Player/Player.swift +++ b/Sources/RolePlayingCore/Player/Player.swift @@ -28,7 +28,7 @@ public extension Dice { } /// The base class for a player character, including its background, species, class, abilities, skills, hit points, and so on. -public class Player: Codable { +public class Player: CodableWithConfiguration { /// The player's name. public var name: String @@ -137,7 +137,7 @@ public class Player: Codable { case money } - public required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder, configuration: Configuration) throws { let values = try decoder.container(keyedBy: CodingKeys.self) // Try decoding properties @@ -156,7 +156,7 @@ public class Player: Codable { let currentHitPoints = try values.decodeIfPresent(Int.self, forKey: .currentHitPoints) let experiencePoints = try values.decodeIfPresent(Int.self, forKey: .experiencePoints) let level = try values.decodeIfPresent(Int.self, forKey: .level) - let money = try values.decode(Money.self, forKey: .money) + let money = try values.decode(Money.self, forKey: .money, configuration: configuration.currencies) // Safely set properties self.name = name @@ -177,7 +177,7 @@ public class Player: Codable { self.money = money } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder, configuration: Configuration) throws { var values = encoder.container(keyedBy: CodingKeys.self) // Try decoding properties @@ -196,7 +196,7 @@ public class Player: Codable { try values.encodeIfPresent(currentHitPoints, forKey: .currentHitPoints) try values.encodeIfPresent(experiencePoints, forKey: .experiencePoints) try values.encodeIfPresent(level, forKey: .level) - try values.encode("\(money)", forKey: .money) + try values.encode(money, forKey: .money, configuration: configuration.currencies) } // Creates a player character. diff --git a/Sources/RolePlayingCore/Player/Players.swift b/Sources/RolePlayingCore/Player/Players.swift index 03b90b3..4f37fc8 100644 --- a/Sources/RolePlayingCore/Player/Players.swift +++ b/Sources/RolePlayingCore/Player/Players.swift @@ -35,8 +35,12 @@ extension Player { } /// A collection of player characters. -public class Players: Codable { - public var players = [Player]() +public class Players: CodableWithConfiguration { + public var players: [Player] + + public init(_ players: [Player] = []) { + self.players = players + } public func resolve(backgrounds: Backgrounds, classes: Classes, species: Species) throws { for player in players { @@ -63,4 +67,20 @@ public class Players: Codable { public func remove(at index: Int) { players.remove(at: index) } + + // MARK: Codable conformance + + private enum CodingKeys: String, CodingKey { + case players + } + + public required init(from decoder: Decoder, configuration: Configuration) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + players = try container.decode([Player].self, forKey: .players, configuration: configuration) + } + + public func encode(to encoder: Encoder, configuration: Configuration) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(players, forKey: .players, configuration: configuration) + } } diff --git a/Tests/RolePlayingCoreTests/CurrencyTests.swift b/Tests/RolePlayingCoreTests/CurrencyTests.swift index dddc7cd..599f8f8 100644 --- a/Tests/RolePlayingCoreTests/CurrencyTests.swift +++ b/Tests/RolePlayingCoreTests/CurrencyTests.swift @@ -10,6 +10,21 @@ import Testing @testable import RolePlayingCore import Foundation +struct MoneyContainer: DecodableWithConfiguration { + typealias DecodingConfiguration = Currencies + + let money: Money! + + private enum CodingKeys: String, CodingKey { + case money + } + + init(from decoder: any Decoder, configuration: RolePlayingCore.Currencies) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + money = try container.decode(Money.self, forKey: .money, configuration: configuration) + } +} + @Suite("Currency Tests") struct UnitCurrencyTests { @@ -25,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.find("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.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 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.find("cp")!) #expect(abs(totalPiecesInCopper.value - 719) < 0.01, "adding coins converted to copper should equal 719") } @@ -57,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.find("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.find("pp")!) let ppProvided = formatter.string(from: platinumPieces) #expect(ppProvided == "1.37 pp", "platinum pieces") @@ -91,13 +106,13 @@ struct UnitCurrencyTests { let gp = Money(value: 2.5, unit: .baseUnit()) #expect(gp.value == 2.5, "coinage as Double should be 2.5") - let cp = "3.2 cp".parseMoney + 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.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") - let invalid = "hello".parseMoney + let invalid = "hello".parseMoney(currencies) #expect(invalid == nil, "coinage as string with hello should be nil") } @@ -110,12 +125,12 @@ struct UnitCurrencyTests { @Test("Duplicate currencies are ignored") func duplicateCurrencies() async throws { - #expect(Currencies.all.count == 5, "currencies count") + #expect(currencies.all.count == 5, "currencies count") let data = try bundle.loadJSON("TestCurrencies") _ = try decoder.decode(Currencies.self, from: data) - #expect(Currencies.all.count == 5, "currencies count should remain 5") + #expect(currencies.all.count == 5, "currencies count should remain 5") } @Test("Missing currency traits") @@ -167,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.find("sp")!)) let encoder = JSONEncoder() let encoded = try encoder.encode(moneyContainer) let deserialized = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: String] @@ -177,9 +192,6 @@ struct UnitCurrencyTests { @Test("Decoding money from string and number") func decodingMoney() async throws { - struct MoneyContainer: Decodable { - let money: Money - } let decoder = JSONDecoder() // Test parseable string @@ -188,8 +200,8 @@ struct UnitCurrencyTests { "money": "72.17 ep" } """.data(using: .utf8)! - let stringContainer = try decoder.decode(MoneyContainer.self, from: stringMoney) - #expect("\(stringContainer.money)" == "72.17 ep", "Decoded money from string") + let stringContainer = try decoder.decode(MoneyContainer.self, from: stringMoney, configuration: currencies) + #expect("\(stringContainer.money!)" == "72.17 ep", "Decoded money from string") // Test raw number let numberMoney = """ @@ -197,8 +209,8 @@ struct UnitCurrencyTests { "money": 85 } """.data(using: .utf8)! - let numberContainer = try decoder.decode(MoneyContainer.self, from: numberMoney) - #expect("\(numberContainer.money)" == "85.0 gp", "Decoded money from number") + let numberContainer = try decoder.decode(MoneyContainer.self, from: numberMoney, configuration: currencies) + #expect("\(numberContainer.money!)" == "85.0 gp", "Decoded money from number") // Test invalid value let invalidMoney = """ @@ -208,15 +220,12 @@ struct UnitCurrencyTests { """.data(using: .utf8)! #expect(throws: (any Error).self) { - _ = try decoder.decode(MoneyContainer.self, from: invalidMoney) + _ = try decoder.decode(MoneyContainer.self, from: invalidMoney, configuration: currencies) } } @Test("Decoding optional money") func decodingMoneyIfPresent() async throws { - struct MoneyContainer: Decodable { - let money: Money? - } let decoder = JSONDecoder() // Test parseable string @@ -225,7 +234,7 @@ struct UnitCurrencyTests { "money": "72.17 ep" } """.data(using: .utf8)! - let stringContainer = try decoder.decode(MoneyContainer.self, from: stringMoney) + let stringContainer = try decoder.decode(MoneyContainer.self, from: stringMoney, configuration: currencies) #expect("\(stringContainer.money!)" == "72.17 ep", "Decoded money from string") // Test raw number @@ -234,7 +243,7 @@ struct UnitCurrencyTests { "money": 85 } """.data(using: .utf8)! - let numberContainer = try decoder.decode(MoneyContainer.self, from: numberMoney) + let numberContainer = try decoder.decode(MoneyContainer.self, from: numberMoney, configuration: currencies) #expect("\(numberContainer.money!)" == "85.0 gp", "Decoded money from number") // Test invalid value should result in nil for optional @@ -243,8 +252,10 @@ struct UnitCurrencyTests { "money": "no money" } """.data(using: .utf8)! - let invalidContainer = try decoder.decode(MoneyContainer.self, from: invalidMoney) - #expect(invalidContainer.money == nil, "decoded invalid money string should be nil") + + #expect(throws: (any Error).self) { + _ = try decoder.decode(MoneyContainer.self, from: invalidMoney, configuration: currencies) + } } @Test("Encoding currencies") diff --git a/Tests/RolePlayingCoreTests/PlayerTests.swift b/Tests/RolePlayingCoreTests/PlayerTests.swift index 32146e7..0c905d9 100644 --- a/Tests/RolePlayingCoreTests/PlayerTests.swift +++ b/Tests/RolePlayingCoreTests/PlayerTests.swift @@ -14,6 +14,7 @@ import Foundation struct PlayerTests { let decoder = JSONDecoder() + let configuration: Configuration let skillTraits: Data let skills: Skills let soldierTraits: Data @@ -24,11 +25,7 @@ struct PlayerTests { let fighter: ClassTraits init() throws { - // TODO: Need to initialize UnitCurrency before creating Money instances in Player class. - // Only load once. TODO: this has a side effect on other unit tests: currencies are already loaded. - let bundle = Bundle.module - let data = try! bundle.loadJSON("TestCurrencies") - _ = try! decoder.decode(Currencies.self, from: data) + configuration = try Configuration("TestConfiguration", from: .module) self.skillTraits = """ { @@ -203,7 +200,7 @@ struct PlayerTests { } """.data(using: .utf8)! - let player = try decoder.decode(Player.self, from: playerTraits) + let player = try decoder.decode(Player.self, from: playerTraits, configuration: configuration) player.speciesTraits = human player.classTraits = fighter @@ -248,7 +245,7 @@ struct PlayerTests { } """.data(using: .utf8)! - let player = try decoder.decode(Player.self, from: playerTraits) + let player = try decoder.decode(Player.self, from: playerTraits, configuration: configuration) player.speciesTraits = human player.classTraits = fighter @@ -289,9 +286,9 @@ struct PlayerTests { } """.data(using: .utf8)! - let player = try #require(try? decoder.decode(Player.self, from: playerTraits)) + let player = try #require(try? decoder.decode(Player.self, from: playerTraits, configuration: configuration)) let encoder = JSONEncoder() - let encodedPlayer = try encoder.encode(player) + let encodedPlayer = try encoder.encode(player, configuration: configuration) let encoded = try #require(try? JSONSerialization.jsonObject(with: encodedPlayer, options: []) as? [String: Any]) #expect(encoded["name"] as? String == "Bilbo", "player traits round trip name") @@ -348,7 +345,7 @@ struct PlayerTests { ]) func missingTraits(json: String) async throws { let traits = json.data(using: .utf8)! - let player = try? decoder.decode(Player.self, from: traits) + let player = try? decoder.decode(Player.self, from: traits, configuration: configuration) #expect(player == nil) } @@ -564,14 +561,14 @@ struct PlayerTests { } """.data(using: .utf8)! - let player = try decoder.decode(Player.self, from: playerTraits) + let player = try decoder.decode(Player.self, from: playerTraits, configuration: configuration) #expect(player.descriptiveTraits.count == 3) #expect(player.descriptiveTraits["ideal"] == "Adventure") #expect(player.descriptiveTraits["bond"] == "The Shire") #expect(player.descriptiveTraits["flaw"] == "Impulsive") // Test encoding - let encoded = try encoder.encode(player) + let encoded = try encoder.encode(player, configuration: configuration) let decodedDict = try #require(try? JSONSerialization.jsonObject(with: encoded) as? [String: Any]) let encodedTraits = try #require(decodedDict["descriptive traits"] as? [String: String]) #expect(encodedTraits["ideal"] == "Adventure") @@ -607,7 +604,7 @@ struct PlayerTests { } """.data(using: .utf8)! - let player = try decoder.decode(Player.self, from: playerTraits) + let player = try decoder.decode(Player.self, from: playerTraits, configuration: configuration) player.speciesTraits = human player.classTraits = fighter diff --git a/Tests/RolePlayingCoreTests/PlayersTests.swift b/Tests/RolePlayingCoreTests/PlayersTests.swift index 72eba74..5e0d259 100644 --- a/Tests/RolePlayingCoreTests/PlayersTests.swift +++ b/Tests/RolePlayingCoreTests/PlayersTests.swift @@ -15,34 +15,17 @@ struct PlayersTests { let bundle = Bundle.module let decoder = JSONDecoder() - let skills: Skills - let backgrounds: Backgrounds - let classes: Classes - let species: Species + let configuration: Configuration init() throws { - // TODO: Need to initialize UnitCurrency before creating Money instances in Player class. - let currenciesData = try! bundle.loadJSON("TestCurrencies") - _ = try! decoder.decode(Currencies.self, from: currenciesData) - - let skillsData = try! bundle.loadJSON("TestSkills") - self.skills = try! decoder.decode(Skills.self, from: skillsData) - - let backgroundsData = try! bundle.loadJSON("TestBackgrounds") - self.backgrounds = try! decoder.decode(Backgrounds.self, from: backgroundsData) - - let classesData = try! bundle.loadJSON("TestClasses") - self.classes = try! decoder.decode(Classes.self, from: classesData) - - let speciesData = try! bundle.loadJSON("TestSpecies") - self.species = try! decoder.decode(Species.self, from: speciesData) + configuration = try Configuration("TestConfiguration", from: .module) } @Test("Load and manipulate players collection") func players() async throws { let playersData = try bundle.loadJSON("TestPlayers") - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(backgrounds: backgrounds, classes: classes, species: species) + let players = try decoder.decode(Players.self, from: playersData, configuration: configuration) + try players.resolve(backgrounds: configuration.backgrounds, classes: configuration.classes, species: configuration.species) #expect(players.players.count == 2, "players count") #expect(players.count == 2, "players count") @@ -68,8 +51,8 @@ struct PlayersTests { // Attempt to decode and resolve, expecting an error to be thrown // Error could occur during decoding (missing required fields) or resolution (invalid references) do { - let players = try decoder.decode(Players.self, from: playersData) - try players.resolve(backgrounds: backgrounds, classes: classes, species: species) + let players = try decoder.decode(Players.self, from: playersData, configuration: configuration) + try players.resolve(backgrounds: configuration.backgrounds, classes: configuration.classes, species: configuration.species) // If we reach here, no error was thrown - the test should fail Issue.record("Expected an error to be thrown for \(jsonFile), but none was thrown") From 719a28b35bce66466f13195673ba4eb13983e7ec Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sat, 8 Nov 2025 15:25:32 -0500 Subject: [PATCH 24/33] Updated Player to support CodableWithConfiguration for resolving backgrounds, classes, and species. --- .../Configuration/Configuration.swift | 1 - Sources/RolePlayingCore/Player/Player.swift | 55 +++++++++++-------- Sources/RolePlayingCore/Player/Players.swift | 34 ------------ Tests/RolePlayingCoreTests/PlayersTests.swift | 8 +-- 4 files changed, 34 insertions(+), 64 deletions(-) diff --git a/Sources/RolePlayingCore/Configuration/Configuration.swift b/Sources/RolePlayingCore/Configuration/Configuration.swift index 41f5c06..3063d49 100644 --- a/Sources/RolePlayingCore/Configuration/Configuration.swift +++ b/Sources/RolePlayingCore/Configuration/Configuration.swift @@ -97,7 +97,6 @@ public struct Configuration { for playersFile in playersFiles { let jsonData = try bundle.loadJSON(playersFile) let players = try jsonDecoder.decode(Players.self, from: jsonData, configuration: self) - try players.resolve(backgrounds: self.backgrounds, classes: self.classes, species: self.species) self.players.players += players.players } } diff --git a/Sources/RolePlayingCore/Player/Player.swift b/Sources/RolePlayingCore/Player/Player.swift index 51e7179..bed9fed 100644 --- a/Sources/RolePlayingCore/Player/Player.swift +++ b/Sources/RolePlayingCore/Player/Player.swift @@ -36,28 +36,23 @@ public class Player: CodableWithConfiguration { /// Descriptive traits, such as ideals, bonds, flaws, a background story, etc. public var descriptiveTraits: [String: String] - public private(set) var backgroundName: String + public var backgroundName: String { + return backgroundTraits?.name ?? "" + } - public private(set) var speciesName: String - public private(set) var className: String + public var speciesName: String { + return speciesTraits?.name ?? "" + } + + public var className: String { + return classTraits?.name ?? "" + } public private(set) var skillProficiencies: [String] - public var backgroundTraits: BackgroundTraits! { - didSet { - self.backgroundName = backgroundTraits.name - } - } - public var speciesTraits: SpeciesTraits! { - didSet { - self.speciesName = speciesTraits.name - } - } - public var classTraits: ClassTraits! { - didSet { - self.className = classTraits.name - } - } + public var backgroundTraits: BackgroundTraits! + public var speciesTraits: SpeciesTraits! + public var classTraits: ClassTraits! public enum Gender: String, Codable, CaseIterable { case female = "Female" @@ -158,11 +153,23 @@ public class Player: CodableWithConfiguration { let level = try values.decodeIfPresent(Int.self, forKey: .level) let money = try values.decode(Money.self, forKey: .money, configuration: configuration.currencies) + // Resolve backgroundTraits from configuration + guard let backgroundTraits = configuration.backgrounds.find(backgroundName) else { + throw missingTypeError("background", backgroundName) + } + + // Resolve speciesTraits from configuration + guard let speciesTraits = configuration.species.find(speciesName) else { + throw missingTypeError("species", speciesName) + } + + // Resolve classTraits from configuration + guard let classTraits = configuration.classes.find(className) else { + throw missingTypeError("class", className) + } + // Safely set properties self.name = name - self.backgroundName = backgroundName - self.speciesName = speciesName - self.className = className self.descriptiveTraits = descriptiveTraits ?? [:] self.gender = gender self.alignment = alignment @@ -175,6 +182,9 @@ public class Player: CodableWithConfiguration { self.experiencePoints = experiencePoints ?? 0 self.level = level ?? 1 self.money = money + self.backgroundTraits = backgroundTraits + self.speciesTraits = speciesTraits + self.classTraits = classTraits } public func encode(to encoder: Encoder, configuration: Configuration) throws { @@ -203,9 +213,6 @@ public class Player: CodableWithConfiguration { public init(_ name: String, backgroundTraits: BackgroundTraits, speciesTraits: SpeciesTraits, classTraits: ClassTraits, gender: Gender? = nil, alignment: Alignment? = nil) { self.name = name self.descriptiveTraits = [:] - self.backgroundName = backgroundTraits.name - self.speciesName = speciesTraits.name - self.className = classTraits.name self.backgroundTraits = backgroundTraits self.speciesTraits = speciesTraits self.classTraits = classTraits diff --git a/Sources/RolePlayingCore/Player/Players.swift b/Sources/RolePlayingCore/Player/Players.swift index 4f37fc8..f84f544 100644 --- a/Sources/RolePlayingCore/Player/Players.swift +++ b/Sources/RolePlayingCore/Player/Players.swift @@ -8,32 +8,6 @@ import Foundation -extension Player { - - // TODO: should we instead support some variation of KeyedArchiver? - - func resolveBackgrounds(from backgrounds: Backgrounds) throws { - guard let backgroundTraits = backgrounds.find(self.backgroundName) else { - throw missingTypeError("background", self.backgroundName) - } - self.backgroundTraits = backgroundTraits - } - - func resolveSpecies(from species: Species) throws { - guard let speciesTraits = species.find(self.speciesName) else { - throw missingTypeError("species", self.speciesName) - } - self.speciesTraits = speciesTraits - } - - func resolveClass(from classes: Classes) throws { - guard let classTraits = classes.find(self.className) else { - throw missingTypeError("class", self.className) - } - self.classTraits = classTraits - } -} - /// A collection of player characters. public class Players: CodableWithConfiguration { public var players: [Player] @@ -42,14 +16,6 @@ public class Players: CodableWithConfiguration { self.players = players } - public func resolve(backgrounds: Backgrounds, classes: Classes, species: Species) throws { - for player in players { - try player.resolveBackgrounds(from: backgrounds) - try player.resolveSpecies(from: species) - try player.resolveClass(from: classes) - } - } - // TODO: inherit protocols for these public var count: Int { return players.count } diff --git a/Tests/RolePlayingCoreTests/PlayersTests.swift b/Tests/RolePlayingCoreTests/PlayersTests.swift index 5e0d259..6b8a852 100644 --- a/Tests/RolePlayingCoreTests/PlayersTests.swift +++ b/Tests/RolePlayingCoreTests/PlayersTests.swift @@ -25,7 +25,6 @@ struct PlayersTests { func players() async throws { let playersData = try bundle.loadJSON("TestPlayers") let players = try decoder.decode(Players.self, from: playersData, configuration: configuration) - try players.resolve(backgrounds: configuration.backgrounds, classes: configuration.classes, species: configuration.species) #expect(players.players.count == 2, "players count") #expect(players.count == 2, "players count") @@ -48,11 +47,10 @@ struct PlayersTests { func missingTraits(jsonFile: String) async throws { let playersData = try bundle.loadJSON(jsonFile) - // Attempt to decode and resolve, expecting an error to be thrown - // Error could occur during decoding (missing required fields) or resolution (invalid references) + // Attempt to decode, expecting an error to be thrown during decoding + // since all trait resolution now happens during the decoding phase do { - let players = try decoder.decode(Players.self, from: playersData, configuration: configuration) - try players.resolve(backgrounds: configuration.backgrounds, classes: configuration.classes, species: configuration.species) + _ = try decoder.decode(Players.self, from: playersData, configuration: configuration) // If we reach here, no error was thrown - the test should fail Issue.record("Expected an error to be thrown for \(jsonFile), but none was thrown") From 1209c03da9501955a2869c421aca3fa44a731c5a Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sat, 8 Nov 2025 16:32:57 -0500 Subject: [PATCH 25/33] Refactored to support converting skills arrays from arrays of skill names. --- .../Configuration/Configuration.swift | 4 +- .../Player/BackgroundTraits.swift | 18 +++++--- .../RolePlayingCore/Player/Backgrounds.swift | 29 +++++++++--- .../RolePlayingCore/Player/ClassTraits.swift | 32 ++++++------- Sources/RolePlayingCore/Player/Classes.swift | 24 +++++++++- Sources/RolePlayingCore/Player/Player.swift | 22 ++++++--- Sources/RolePlayingCore/Player/Skills.swift | 13 ++++++ .../BackgroundsTests.swift | 30 ++++++++----- .../ClassTraitsTests.swift | 45 ++++++++++--------- Tests/RolePlayingCoreTests/ClassesTests.swift | 9 +++- Tests/RolePlayingCoreTests/PlayerTests.swift | 4 +- .../TestClassesConfiguration.json | 8 ++++ 12 files changed, 162 insertions(+), 76 deletions(-) create mode 100644 Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json diff --git a/Sources/RolePlayingCore/Configuration/Configuration.swift b/Sources/RolePlayingCore/Configuration/Configuration.swift index 3063d49..562e666 100644 --- a/Sources/RolePlayingCore/Configuration/Configuration.swift +++ b/Sources/RolePlayingCore/Configuration/Configuration.swift @@ -68,7 +68,7 @@ public struct Configuration { for backgroundsFile in configurationFiles.backgrounds { let jsonData = try bundle.loadJSON(backgroundsFile) - let backgrounds = try jsonDecoder.decode(Backgrounds.self, from: jsonData) + let backgrounds = try jsonDecoder.decode(Backgrounds.self, from: jsonData, configuration: self) self.backgrounds.backgrounds += backgrounds.backgrounds } @@ -80,7 +80,7 @@ public struct Configuration { for classFile in configurationFiles.classes { let jsonData = try bundle.loadJSON(classFile) - let classes = try jsonDecoder.decode(Classes.self, from: jsonData) + let classes = try jsonDecoder.decode(Classes.self, from: jsonData, configuration: self) self.classes.classes += classes.classes // Update the shared classes experience points table, then diff --git a/Sources/RolePlayingCore/Player/BackgroundTraits.swift b/Sources/RolePlayingCore/Player/BackgroundTraits.swift index ae2dfd1..4f658b7 100644 --- a/Sources/RolePlayingCore/Player/BackgroundTraits.swift +++ b/Sources/RolePlayingCore/Player/BackgroundTraits.swift @@ -6,17 +6,19 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // +import Foundation + /// Traits associated with a player character's background. public struct BackgroundTraits { public var name: String public var abilityScores: [String] public var feat: String - public var skillProficiencies: [String] + public var skillProficiencies: [Skill] public var toolProficiency: String public var equipment: [[String]] } -extension BackgroundTraits: Codable { +extension BackgroundTraits: CodableWithConfiguration { private enum CodingKeys: String, CodingKey { case name case abilityScores = "ability scores" @@ -26,22 +28,26 @@ extension BackgroundTraits: Codable { case equipment = "equipment" } - public init(from decoder: Decoder) throws { + public init(from decoder: Decoder, configuration: Configuration) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.name = try values.decode(String.self, forKey: .name) self.abilityScores = try values.decode([String].self, forKey: .abilityScores) self.feat = try values.decode(String.self, forKey: .feat) - self.skillProficiencies = try values.decode([String].self, forKey: .skillProficiencies) + + // Decode skill proficiency names and resolve them using configuration + let skillNames = try values.decode([String].self, forKey: .skillProficiencies) + self.skillProficiencies = try skillNames.skills(from: configuration.skills) + self.toolProficiency = try values.decode(String.self, forKey: .toolProficiency) self.equipment = try values.decode([[String]].self, forKey: .equipment) } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder, configuration: Configuration) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(abilityScores, forKey: .abilityScores) try container.encode(feat, forKey: .feat) - try container.encode(skillProficiencies, forKey: .skillProficiencies) + try container.encode(skillProficiencies.skillNames, forKey: .skillProficiencies) try container.encode(toolProficiency, forKey: .toolProficiency) try container.encode(equipment, forKey: .equipment) } diff --git a/Sources/RolePlayingCore/Player/Backgrounds.swift b/Sources/RolePlayingCore/Player/Backgrounds.swift index 6182cae..f975674 100644 --- a/Sources/RolePlayingCore/Player/Backgrounds.swift +++ b/Sources/RolePlayingCore/Player/Backgrounds.swift @@ -6,13 +6,15 @@ // Copyright © 2025 Brian Arnold. All rights reserved. // -/// A collection of background traits. -public struct Backgrounds: Codable { +import Foundation + +/// A collection of backgrounds. +public struct Backgrounds: CodableWithConfiguration { - public var backgrounds = [BackgroundTraits]() + public var backgrounds: [BackgroundTraits] - private enum CodingKeys: String, CodingKey { - case backgrounds + public init(_ backgrounds: [BackgroundTraits] = []) { + self.backgrounds = backgrounds } public func find(_ backgroundName: String?) -> BackgroundTraits? { @@ -23,6 +25,7 @@ public struct Backgrounds: Codable { public subscript(index: Int) -> BackgroundTraits? { get { + guard index >= 0 && index < backgrounds.count else { return nil } return backgrounds[index] } } @@ -30,4 +33,20 @@ public struct Backgrounds: Codable { public func randomElementByIndex(using generator: inout G) -> BackgroundTraits { return backgrounds.randomElementByIndex(using: &generator)! } + + // MARK: Codable conformance + + private enum CodingKeys: String, CodingKey { + case backgrounds + } + + 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) + } + + public func encode(to encoder: Encoder, configuration: Configuration) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(backgrounds, forKey: .backgrounds, configuration: configuration) + } } diff --git a/Sources/RolePlayingCore/Player/ClassTraits.swift b/Sources/RolePlayingCore/Player/ClassTraits.swift index 0777490..a9d366e 100644 --- a/Sources/RolePlayingCore/Player/ClassTraits.swift +++ b/Sources/RolePlayingCore/Player/ClassTraits.swift @@ -21,7 +21,7 @@ public struct ClassTraits { public var savingThrows: [Ability] public var experiencePoints: [Int]? public var startingSkillCount: Int - public var skillProficiencies: [String] + public var skillProficiencies: [Skill] public var weaponProficiencies: [String] public var toolProficiencies: [String] public var armorTraining: [String] @@ -61,7 +61,7 @@ public struct ClassTraits { alternatePrimaryAbility: [Ability]? = nil, savingThrows: [Ability] = [], startingSkillCount: Int = 2, - skillProficiencies: [String] = [], + skillProficiencies: [Skill] = [], weaponProficiencies: [String] = [], toolProficiencies: [String] = [], armorTraining: [String] = [], @@ -86,7 +86,7 @@ public struct ClassTraits { } } -extension ClassTraits: Codable { +extension ClassTraits: CodableWithConfiguration { private enum CodingKeys: String, CodingKey { case name @@ -106,7 +106,7 @@ extension ClassTraits: Codable { case experiencePoints = "experience points" } - public init(from decoder: Decoder) throws { + public init(from decoder: Decoder, configuration: Configuration) throws { let values = try decoder.container(keyedBy: CodingKeys.self) // Try decoding properties @@ -120,7 +120,11 @@ extension ClassTraits: Codable { let alternatePrimaryAbility = try values.decodeIfPresent([Ability].self, forKey: .alternatePrimaryAbility) let savingThrows = try values.decodeIfPresent([Ability].self, forKey: .savingThrows) let startingSkillCount: Int? = try values.decodeIfPresent(Int.self, forKey: .startingSkillCount) - let skillProficiencies = try values.decodeIfPresent([String].self, forKey: .skillProficiencies) + + // Decode skill proficiency names and resolve them using configuration + let skillNames = try values.decodeIfPresent([String].self, forKey: .skillProficiencies) ?? [] + let resolvedSkills: [Skill] = try skillNames.skills(from: configuration.skills) + let weaponProficiencies = try values.decodeIfPresent([String].self, forKey: .weaponProficiencies) let toolProficiencies = try values.decodeIfPresent([String].self, forKey: .toolProficiencies) let armorTraining: [String]? = try values.decodeIfPresent([String].self, forKey: .armorTraining) @@ -139,7 +143,7 @@ extension ClassTraits: Codable { self.alternatePrimaryAbility = alternatePrimaryAbility self.savingThrows = savingThrows ?? [] self.startingSkillCount = startingSkillCount ?? 2 - self.skillProficiencies = skillProficiencies ?? [] + self.skillProficiencies = resolvedSkills self.weaponProficiencies = weaponProficiencies ?? [] self.toolProficiencies = toolProficiencies ?? [] self.armorTraining = armorTraining ?? [] @@ -148,7 +152,7 @@ extension ClassTraits: Codable { self.experiencePoints = experiencePoints } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder, configuration: Configuration) throws { var values = encoder.container(keyedBy: CodingKeys.self) try values.encode(name, forKey: .name) @@ -161,7 +165,7 @@ extension ClassTraits: Codable { try values.encodeIfPresent(alternatePrimaryAbility, forKey: .alternatePrimaryAbility) try values.encode(savingThrows, forKey: .savingThrows) try values.encode(startingSkillCount, forKey: .startingSkillCount) - try values.encode(skillProficiencies, forKey: .skillProficiencies) + try values.encode(skillProficiencies.skillNames, forKey: .skillProficiencies) try values.encode(weaponProficiencies, forKey: .weaponProficiencies) try values.encode(toolProficiencies, forKey: .toolProficiencies) try values.encode(armorTraining, forKey: .armorTraining) @@ -174,16 +178,8 @@ extension ClassTraits: Codable { extension ClassTraits { /// Returns a random array of skill proficiencies, of a count matching startingSkillCount. - public func randomSkillProficiencies() -> [String] { - var selected: [String] = [] - var remaining: [String] = skillProficiencies - - for _ in 0.. [Skill] { + return skillProficiencies.randomSkills(count: startingSkillCount) } } diff --git a/Sources/RolePlayingCore/Player/Classes.swift b/Sources/RolePlayingCore/Player/Classes.swift index 29425e3..fae3f47 100644 --- a/Sources/RolePlayingCore/Player/Classes.swift +++ b/Sources/RolePlayingCore/Player/Classes.swift @@ -6,12 +6,18 @@ // Copyright © 2016 Brian Arnold. All rights reserved. // +import Foundation + /// A collection of class traits. -public struct Classes: Codable { +public struct Classes: CodableWithConfiguration { - public var classes = [ClassTraits]() + public var classes: [ClassTraits] public var experiencePoints: [Int]? + public init(_ classes: [ClassTraits] = []) { + self.classes = classes + } + private enum CodingKeys: String, CodingKey { case classes case experiencePoints = "experience points" @@ -28,4 +34,18 @@ public struct Classes: Codable { return classes[index] } } + + // MARK: CodableWithConfiguration conformance + + public init(from decoder: Decoder, configuration: Configuration) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + self.classes = try values.decode([ClassTraits].self, forKey: .classes, configuration: configuration) + self.experiencePoints = try values.decodeIfPresent([Int].self, forKey: .experiencePoints) + } + + public func encode(to encoder: Encoder, configuration: Configuration) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(classes, forKey: .classes, configuration: configuration) + try container.encodeIfPresent(experiencePoints, forKey: .experiencePoints) + } } diff --git a/Sources/RolePlayingCore/Player/Player.swift b/Sources/RolePlayingCore/Player/Player.swift index bed9fed..83e38aa 100644 --- a/Sources/RolePlayingCore/Player/Player.swift +++ b/Sources/RolePlayingCore/Player/Player.swift @@ -48,7 +48,7 @@ public class Player: CodableWithConfiguration { return classTraits?.name ?? "" } - public private(set) var skillProficiencies: [String] + public private(set) var skillProficiencies: [Skill] public var backgroundTraits: BackgroundTraits! public var speciesTraits: SpeciesTraits! @@ -146,7 +146,17 @@ public class Player: CodableWithConfiguration { let height = try values.decode(Height.self, forKey: .height) let baseAbilities = try values.decode(AbilityScores.self, forKey: .baseAbilities) let backgroundAbilities = try values.decode([String].self, forKey: .backgroundAbilities) - let skillProficiencies = try values.decode([String].self, forKey: .skillProficiencies) + + // Decode skill proficiency names and resolve them using configuration + let skillNames = try values.decode([String].self, forKey: .skillProficiencies) + var resolvedSkills: [Skill] = [] + for skillName in skillNames { + guard let skill = configuration.skills.find(skillName) else { + throw missingTypeError("skill", skillName) + } + resolvedSkills.append(skill) + } + let maximumHitPoints = try values.decode(Int.self, forKey: .maximumHitPoints) let currentHitPoints = try values.decodeIfPresent(Int.self, forKey: .currentHitPoints) let experiencePoints = try values.decodeIfPresent(Int.self, forKey: .experiencePoints) @@ -176,7 +186,7 @@ public class Player: CodableWithConfiguration { self.height = height self.baseAbilities = baseAbilities self.backgroundAbilities = backgroundAbilities.map { Ability($0) } - self.skillProficiencies = skillProficiencies + self.skillProficiencies = resolvedSkills self.maximumHitPoints = maximumHitPoints self.currentHitPoints = currentHitPoints ?? maximumHitPoints self.experiencePoints = experiencePoints ?? 0 @@ -201,7 +211,7 @@ public class Player: CodableWithConfiguration { try values.encode("\(height)", forKey: .height) try values.encode(baseAbilities, forKey: .baseAbilities) try values.encode(backgroundAbilities.map({ $0.name }), forKey: .backgroundAbilities) - try values.encode(skillProficiencies, forKey: .skillProficiencies) + try values.encode(skillProficiencies.skillNames, forKey: .skillProficiencies) try values.encode(maximumHitPoints, forKey: .maximumHitPoints) try values.encodeIfPresent(currentHitPoints, forKey: .currentHitPoints) try values.encodeIfPresent(experiencePoints, forKey: .experiencePoints) @@ -229,7 +239,9 @@ public class Player: CodableWithConfiguration { // TODO: roll for 2 or 3 background abilities, and if 2, add one random ability score twice self.backgroundAbilities = backgroundTraits.abilityScores.map { Ability($0) } - self.skillProficiencies = classTraits.randomSkillProficiencies() + backgroundTraits.skillProficiencies + var allSkills = classTraits.randomSkillProficiencies() + allSkills.append(backgroundTraits.skillProficiencies) + self.skillProficiencies = allSkills self.maximumHitPoints = Player.rollHitPoints(classTraits: classTraits, speciesTraits: speciesTraits) self.currentHitPoints = self.maximumHitPoints diff --git a/Sources/RolePlayingCore/Player/Skills.swift b/Sources/RolePlayingCore/Player/Skills.swift index ec084fa..88597c2 100644 --- a/Sources/RolePlayingCore/Player/Skills.swift +++ b/Sources/RolePlayingCore/Player/Skills.swift @@ -38,6 +38,19 @@ public struct Skills: Codable { } } +extension Sequence where Element == String { + + /// Returns an array of skills from this array of skill names, using the skills argument. + public func skills(from skills: Skills) throws -> [Skill] { + try self.map { skillName in + guard let skill = skills.find(skillName) else { + throw missingTypeError("skill", skillName) + } + return skill + } + } +} + extension Sequence where Element == Skill { /// Returns a random array of skills with the specified skill count. diff --git a/Tests/RolePlayingCoreTests/BackgroundsTests.swift b/Tests/RolePlayingCoreTests/BackgroundsTests.swift index 54f19f8..a613575 100644 --- a/Tests/RolePlayingCoreTests/BackgroundsTests.swift +++ b/Tests/RolePlayingCoreTests/BackgroundsTests.swift @@ -14,6 +14,11 @@ import Foundation struct BackgroundsTests { let decoder = JSONDecoder() + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } @Test("Decode background traits") func backgroundTraitsDecoding() async throws { @@ -29,15 +34,15 @@ struct BackgroundsTests { } """.data(using: .utf8)! - // When: Decoding the JSON data - let background = try decoder.decode(BackgroundTraits.self, from: jsonData) + // When: Decoding the JSON data with configuration + let background = try decoder.decode(BackgroundTraits.self, from: jsonData, configuration: configuration) // Then: The properties should match the input #expect(background.name == "Acolyte", "Name should match") #expect(background.abilityScores == ["Intelligence", "Wisdom"], "Ability scores should match") #expect(background.feat == "Magic Initiate", "Feat should match") #expect(background.skillProficiencies.count == 2, "Should have 2 skill proficiencies") - #expect(background.skillProficiencies == ["Insight", "Religion"], "Skill names should match") + #expect(background.skillProficiencies.skillNames == ["Insight", "Religion"], "Skill names should match") #expect(background.toolProficiency == "Calligrapher's Supplies", "Tool proficiency should match") #expect(background.equipment.count == 2, "Should have 2 equipment choices") #expect(background.equipment[0] == ["Holy Symbol", "Prayer Book", "Vestments", "10 GP"], "First equipment choice should match") @@ -57,19 +62,20 @@ struct BackgroundsTests { } """.data(using: .utf8)! - let background = try decoder.decode(BackgroundTraits.self, from: jsonData) + let background = try decoder.decode(BackgroundTraits.self, from: jsonData, configuration: configuration) + // When: Encoding the background back to JSON let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let encodedData = try encoder.encode(background) + let encodedData = try encoder.encode(background, configuration: configuration) // Then: The encoded data should be decodable and match the original - let decodedBackground = try decoder.decode(BackgroundTraits.self, from: encodedData) + let decodedBackground = try decoder.decode(BackgroundTraits.self, from: encodedData, configuration: configuration) #expect(decodedBackground.name == background.name, "Name should match after round-trip") #expect(decodedBackground.abilityScores == background.abilityScores, "Ability scores should match after round-trip") #expect(decodedBackground.feat == background.feat, "Feat should match after round-trip") - #expect(decodedBackground.skillProficiencies == background.skillProficiencies, "Skills should match after round-trip") + #expect(decodedBackground.skillProficiencies.skillNames == background.skillProficiencies.skillNames, "Skills should match after round-trip") #expect(decodedBackground.toolProficiency == background.toolProficiency, "Tool proficiency should match after round-trip") #expect(decodedBackground.equipment == background.equipment, "Equipment should match after round-trip") } @@ -108,8 +114,8 @@ struct BackgroundsTests { } """.data(using: .utf8)! - // When: Decoding the JSON data into a Backgrounds collection - let backgrounds = try decoder.decode(Backgrounds.self, from: jsonData) + // When: Decoding the JSON data into a Backgrounds collection with configuration + let backgrounds = try decoder.decode(Backgrounds.self, from: jsonData, configuration: configuration) // Then: The collection should have the correct count #expect(backgrounds.count == 3, "Should have 3 backgrounds") @@ -120,7 +126,7 @@ struct BackgroundsTests { #expect(acolyte.feat == "Magic Initiate", "Acolyte feat should match") let criminal = try #require(backgrounds.find("Criminal")) - #expect(criminal.skillProficiencies == ["Deception", "Stealth"], "Criminal skills should match") + #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") @@ -136,8 +142,8 @@ struct BackgroundsTests { // Then: Round-trip encoding should preserve data let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys] - let encodedData = try encoder.encode(backgrounds) - let decodedBackgrounds = try decoder.decode(Backgrounds.self, from: encodedData) + let encodedData = try encoder.encode(backgrounds, configuration: configuration) + 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") diff --git a/Tests/RolePlayingCoreTests/ClassTraitsTests.swift b/Tests/RolePlayingCoreTests/ClassTraitsTests.swift index f651986..43d92d7 100644 --- a/Tests/RolePlayingCoreTests/ClassTraitsTests.swift +++ b/Tests/RolePlayingCoreTests/ClassTraitsTests.swift @@ -15,6 +15,11 @@ struct ClassTraitsTests { let decoder = JSONDecoder() let bundle = Bundle.module + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestClassesConfiguration", from: .module) + } @Test("Decoding class traits with nominal required traits") func decodingNominalClassTraits() throws { @@ -30,7 +35,7 @@ struct ClassTraitsTests { } """.data(using: .utf8)! - let classTraits = try decoder.decode(ClassTraits.self, from: traits) + let classTraits = try decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits.name == "Fighter", "name") #expect(classTraits.plural == "Fighters", "plural") @@ -62,7 +67,7 @@ struct ClassTraitsTests { } """.data(using: .utf8)! - let classTraits = try decoder.decode(ClassTraits.self, from: traits) + let classTraits = try decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits.name == "Fighter", "name") #expect(classTraits.plural == "Fighters", "plural") @@ -98,7 +103,7 @@ struct ClassTraitsTests { } """.data(using: .utf8)! - let classTraits = try decoder.decode(ClassTraits.self, from: traits) + let classTraits = try decoder.decode(ClassTraits.self, from: traits, configuration: configuration) let experiencePoints: [Int] = classTraits.experiencePoints ?? [] #expect(experiencePoints == [300, 900, 2700], "experience points") @@ -113,7 +118,7 @@ struct ClassTraitsTests { hitDice: SimpleDice(.d10), startingWealth: CompoundDice(.d4, times: 5, modifier: 10, mathOperator: "x")) - let encoded = try encoder.encode(classTraits) + let encoded = try encoder.encode(classTraits, configuration: configuration) let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as? [String: Any] #expect(dictionary?["name"] as? String == "Fighter", "name") #expect(dictionary?["plural"] as? String == "Fighters", "plural") @@ -126,7 +131,7 @@ struct ClassTraitsTests { // Test that each missing trait results in nil do { let traits = "{}".data(using: .utf8)! - let classTraits = try? decoder.decode(ClassTraits.self, from: traits) + let classTraits = try? decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits == nil) } @@ -136,7 +141,7 @@ struct ClassTraitsTests { "name": "Fighter" } """.data(using: .utf8)! - let classTraits = try? decoder.decode(ClassTraits.self, from: traits) + let classTraits = try? decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits == nil) } @@ -147,7 +152,7 @@ struct ClassTraitsTests { "plural": "Fighters" } """.data(using: .utf8)! - let classTraits = try? decoder.decode(ClassTraits.self, from: traits) + let classTraits = try? decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits == nil) } @@ -159,7 +164,7 @@ struct ClassTraitsTests { "hit dice": "d10" } """.data(using: .utf8)! - let classTraits = try? decoder.decode(ClassTraits.self, from: traits) + let classTraits = try? decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits == nil) } } @@ -178,7 +183,7 @@ struct ClassTraitsTests { } """.data(using: .utf8)! - let classTraits = try decoder.decode(ClassTraits.self, from: traits) + let classTraits = try decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits.experiencePoints != nil, "Should decode empty array") #expect(classTraits.experiencePoints?.count == 0, "Should have 0 experience points") #expect(classTraits.maxLevel == 0, "Max level should be 0 for empty array") @@ -198,7 +203,7 @@ struct ClassTraitsTests { } """.data(using: .utf8)! - let classTraits = try decoder.decode(ClassTraits.self, from: traits) + let classTraits = try decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits.maxLevel == 1, "Max level should be 1") #expect(classTraits.minExperiencePoints(at: 1) == 0, "Min XP at level 1 should be 0") #expect(classTraits.minExperiencePoints(at: 2) == 0, "Beyond max level should return last value") @@ -254,7 +259,7 @@ struct ClassTraitsTests { } """.data(using: .utf8)! - let classTraits = try decoder.decode(ClassTraits.self, from: traits) + let classTraits = try decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits.descriptiveTraits.count == 0, "Descriptive traits should be empty") #expect(classTraits.primaryAbility.count == 0, "Primary ability should be empty") @@ -282,7 +287,7 @@ struct ClassTraitsTests { } """.data(using: .utf8)! - let classTraits = try decoder.decode(ClassTraits.self, from: traits) + let classTraits = try decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits.primaryAbility.count == 2, "Should have 2 primary abilities") #expect(classTraits.primaryAbility == [Ability("Strength"), Ability("Dexterity")]) @@ -308,7 +313,7 @@ struct ClassTraitsTests { } """.data(using: .utf8)! - let classTraits = try decoder.decode(ClassTraits.self, from: traits) + let classTraits = try decoder.decode(ClassTraits.self, from: traits, configuration: configuration) #expect(classTraits.startingEquipment.count == 5, "Should have 5 equipment choices") #expect(classTraits.startingEquipment[0] == ["Longsword", "Shield"]) @@ -319,13 +324,9 @@ struct ClassTraitsTests { @Test("Round-trip encoding with all fields") func roundTripEncodingWithAllFields() throws { let encoder = JSONEncoder() - let skillsData = try! bundle.loadJSON("TestSkills") - let allSkills = try! decoder.decode(Skills.self, from: skillsData) - var classSkills: [Skill] = [] - for skillName in ["Acrobatics", "Performance", "Persuasion"] { - classSkills.append(allSkills.find(skillName)!) - } + let skillProficiencyNames = ["Athletics", "Acrobatics", "Performance", "Persuasion"] + let skillProficiencies = try skillProficiencyNames.skills(from: configuration.skills) let original = ClassTraits( name: "Bard", plural: "Bards", @@ -336,7 +337,7 @@ struct ClassTraitsTests { alternatePrimaryAbility: [Ability("Dexterity")], savingThrows: [Ability("Dexterity"), Ability("Charisma")], startingSkillCount: 3, - skillProficiencies: ["Atheletics", "Acrobatics", "Performance", "Persuasion"], + skillProficiencies: skillProficiencies, weaponProficiencies: ["Simple Weapons", "Hand Crossbows", "Longswords", "Rapiers", "Shortswords"], toolProficiencies: ["Three Musical Instruments"], armorTraining: ["Light Armor"], @@ -344,8 +345,8 @@ struct ClassTraitsTests { experiencePoints: [0, 300, 900, 2700, 6500, 14000] ) - let encoded = try encoder.encode(original) - let decoded = try decoder.decode(ClassTraits.self, from: encoded) + let encoded = try encoder.encode(original, configuration: configuration) + let decoded = try decoder.decode(ClassTraits.self, from: encoded, configuration: configuration) #expect(decoded.name == original.name, "Name should match after round-trip") #expect(decoded.plural == original.plural, "Plural should match") diff --git a/Tests/RolePlayingCoreTests/ClassesTests.swift b/Tests/RolePlayingCoreTests/ClassesTests.swift index ad9ddfd..12a27e6 100644 --- a/Tests/RolePlayingCoreTests/ClassesTests.swift +++ b/Tests/RolePlayingCoreTests/ClassesTests.swift @@ -15,11 +15,16 @@ struct ClassesTests { let bundle = Bundle.module let decoder = JSONDecoder() + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestClassesConfiguration", from: .module) + } @Test("Default classes") func defaultClasses() throws { let jsonData = try bundle.loadJSON("TestClasses") - let classes = try decoder.decode(Classes.self, from: jsonData) + let classes = try decoder.decode(Classes.self, from: jsonData, configuration: configuration) #expect(classes.classes.count == 4, "classes count failed") #expect(classes.count == 4, "classes count failed") #expect(classes[0] != nil, "class by index failed") @@ -35,7 +40,7 @@ struct ClassesTests { @Test("Uncommon classes") func uncommonClasses() throws { let jsonData = try bundle.loadJSON("TestMoreClasses") - let classes = try decoder.decode(Classes.self, from: jsonData) + let classes = try decoder.decode(Classes.self, from: jsonData, configuration: configuration) #expect(classes.classes.count == 8, "classes count failed") } diff --git a/Tests/RolePlayingCoreTests/PlayerTests.swift b/Tests/RolePlayingCoreTests/PlayerTests.swift index 0c905d9..3698ff4 100644 --- a/Tests/RolePlayingCoreTests/PlayerTests.swift +++ b/Tests/RolePlayingCoreTests/PlayerTests.swift @@ -117,7 +117,7 @@ struct PlayerTests { "equipment": [["Spear", "Shortbow", "20 Arrows", "Gaming Set", "Healer's Kit", "Quiver", "Traveler's Clothes", "14 GP"], ["50 GP"]] } """.data(using: .utf8)! - self.soldier = try! decoder.decode(BackgroundTraits.self, from: self.soldierTraits) + self.soldier = try! decoder.decode(BackgroundTraits.self, from: self.soldierTraits, configuration: configuration) self.fighterTraits = """ { @@ -131,7 +131,7 @@ struct PlayerTests { "experience points": [0, 300, 900, 2700] } """.data(using: .utf8)! - self.fighter = try! decoder.decode(ClassTraits.self, from: self.fighterTraits) + self.fighter = try! decoder.decode(ClassTraits.self, from: self.fighterTraits, configuration: configuration) self.humanTraits = """ { diff --git a/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json b/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json new file mode 100644 index 0000000..09157d3 --- /dev/null +++ b/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json @@ -0,0 +1,8 @@ +{ + "currencies": ["TestCurrencies"], + "skills": ["TestSkills"], + "backgrounds": [], + "classes": [], + "species": [], + "players": [] +} From 1c4dd87ccd19be5363d665fd476249e327a6eee9 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sat, 8 Nov 2025 16:36:27 -0500 Subject: [PATCH 26/33] Rearranged some properties. --- Sources/RolePlayingCore/Player/Player.swift | 28 ++++++++------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/Sources/RolePlayingCore/Player/Player.swift b/Sources/RolePlayingCore/Player/Player.swift index 83e38aa..9d9e34e 100644 --- a/Sources/RolePlayingCore/Player/Player.swift +++ b/Sources/RolePlayingCore/Player/Player.swift @@ -33,27 +33,19 @@ public class Player: CodableWithConfiguration { /// The player's name. public var name: String - /// Descriptive traits, such as ideals, bonds, flaws, a background story, etc. - public var descriptiveTraits: [String: String] - - public var backgroundName: String { - return backgroundTraits?.name ?? "" - } - - public var speciesName: String { - return speciesTraits?.name ?? "" - } - - public var className: String { - return classTraits?.name ?? "" - } - - public private(set) var skillProficiencies: [Skill] - public var backgroundTraits: BackgroundTraits! public var speciesTraits: SpeciesTraits! public var classTraits: ClassTraits! - + + public var backgroundName: String { backgroundTraits?.name ?? "" } + public var speciesName: String { speciesTraits?.name ?? "" } + public var className: String { classTraits?.name ?? "" } + + public private(set) var skillProficiencies: [Skill] + + /// Descriptive traits, such as ideals, bonds, flaws, a background story, etc. + public var descriptiveTraits: [String: String] + public enum Gender: String, Codable, CaseIterable { case female = "Female" case male = "Male" From 12b6230e0cbb06d3538367f1ab60b6e21f423ce2 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sat, 8 Nov 2025 16:40:21 -0500 Subject: [PATCH 27/33] Updates from the core library. --- .../CharacterGenerator/Player/CharacterSheet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift index 349e18e..419e2d9 100644 --- a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/CharacterSheet.swift @@ -84,7 +84,7 @@ class CharacterSheet { } var abilities: AbilityScores { player.abilities } var skills: String { - player.skillProficiencies.joined(separator: ", ") + player.skillProficiencies.skillNames.joined(separator: ", ") } var initiative: String { player.initiativeModifier.displayModifier } var armorClass: String { "\(player.armorClass)" } From 0c52b2f339e5e77100a1923007d2da60f0c8d9af Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sat, 8 Nov 2025 19:49:14 -0500 Subject: [PATCH 28/33] Generalized CreatureType. --- .../Configuration/Species.json | 16 +++++++ .../Configuration/Configuration.swift | 2 +- .../RolePlayingCore/Player/Alignment.swift | 3 -- .../RolePlayingCore/Player/Backgrounds.swift | 10 ++--- .../RolePlayingCore/Player/ClassTraits.swift | 1 - Sources/RolePlayingCore/Player/Classes.swift | 7 ++- .../RolePlayingCore/Player/CreatureType.swift | 24 ++++++++++ Sources/RolePlayingCore/Player/Species.swift | 44 +++++++++++++------ .../Player/SpeciesTraits.swift | 30 +++---------- Tests/RolePlayingCoreTests/PlayerTests.swift | 2 +- .../SpeciesNamesTests.swift | 10 ++++- Tests/RolePlayingCoreTests/SpeciesTests.swift | 10 +++-- .../SpeciesTraitsTests.swift | 27 +++++++----- .../TestResources/TestSpecies.json | 18 +++++++- 14 files changed, 134 insertions(+), 70 deletions(-) create mode 100644 Sources/RolePlayingCore/Player/CreatureType.swift diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json index 68e61f8..e717b81 100644 --- a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json @@ -1,4 +1,20 @@ { + "creature types": [ + { "name": "Aberration" }, + { "name": "Beast" }, + { "name": "Celestial" }, + { "name": "Construct" }, + { "name": "Dragon" }, + { "name": "Elemental" }, + { "name": "Fey" }, + { "name": "Fiend" }, + { "name": "Giant" }, + { "name": "Humanoid", "is default": true }, + { "name": "Monstrosity" }, + { "name": "Ooze" }, + { "name": "Plant" }, + { "name": "Undead" } + ], "species": [ { "name": "Aasimar", diff --git a/Sources/RolePlayingCore/Configuration/Configuration.swift b/Sources/RolePlayingCore/Configuration/Configuration.swift index 562e666..fd0ce95 100644 --- a/Sources/RolePlayingCore/Configuration/Configuration.swift +++ b/Sources/RolePlayingCore/Configuration/Configuration.swift @@ -74,7 +74,7 @@ public struct Configuration { for speciesFile in configurationFiles.species { let jsonData = try bundle.loadJSON(speciesFile) - let species = try jsonDecoder.decode(Species.self, from: jsonData) + let species = try jsonDecoder.decode(Species.self, from: jsonData, configuration: self) self.species.species += species.species } diff --git a/Sources/RolePlayingCore/Player/Alignment.swift b/Sources/RolePlayingCore/Player/Alignment.swift index 4b8f2ab..cbba232 100644 --- a/Sources/RolePlayingCore/Player/Alignment.swift +++ b/Sources/RolePlayingCore/Player/Alignment.swift @@ -39,7 +39,6 @@ extension Ethics: CustomStringConvertible { /// A measure of goodness vs. evil. public enum Morals: String, CaseIterable { - case good = "Good" case neutral = "Neutral" case evil = "Evil" @@ -100,7 +99,6 @@ public struct Alignment { /// A combination of ethics and morals enumerations. public struct Kind { - public let ethics: Ethics public let morals: Morals @@ -115,7 +113,6 @@ public struct Alignment { self.ethics = ethics self.morals = morals } - } internal let valueRange = -1.0...1.0 diff --git a/Sources/RolePlayingCore/Player/Backgrounds.swift b/Sources/RolePlayingCore/Player/Backgrounds.swift index f975674..14657ca 100644 --- a/Sources/RolePlayingCore/Player/Backgrounds.swift +++ b/Sources/RolePlayingCore/Player/Backgrounds.swift @@ -17,17 +17,15 @@ public struct Backgrounds: CodableWithConfiguration { self.backgrounds = backgrounds } - public func find(_ backgroundName: String?) -> BackgroundTraits? { + public func find(_ backgroundName: String) -> BackgroundTraits? { return backgrounds.first(where: { $0.name == backgroundName }) } - public var count: Int { return backgrounds.count } + public var count: Int { backgrounds.count } public subscript(index: Int) -> BackgroundTraits? { - get { - guard index >= 0 && index < backgrounds.count else { return nil } - return backgrounds[index] - } + guard index >= 0 && index < backgrounds.count else { return nil } + return backgrounds[index] } public func randomElementByIndex(using generator: inout G) -> BackgroundTraits { diff --git a/Sources/RolePlayingCore/Player/ClassTraits.swift b/Sources/RolePlayingCore/Player/ClassTraits.swift index a9d366e..2155215 100644 --- a/Sources/RolePlayingCore/Player/ClassTraits.swift +++ b/Sources/RolePlayingCore/Player/ClassTraits.swift @@ -181,5 +181,4 @@ extension ClassTraits { public func randomSkillProficiencies() -> [Skill] { return skillProficiencies.randomSkills(count: startingSkillCount) } - } diff --git a/Sources/RolePlayingCore/Player/Classes.swift b/Sources/RolePlayingCore/Player/Classes.swift index fae3f47..7e826fa 100644 --- a/Sources/RolePlayingCore/Player/Classes.swift +++ b/Sources/RolePlayingCore/Player/Classes.swift @@ -27,12 +27,11 @@ public struct Classes: CodableWithConfiguration { return classes.first(where: { $0.name == className }) } - public var count: Int { return classes.count } + public var count: Int { classes.count } public subscript(index: Int) -> ClassTraits? { - get { - return classes[index] - } + guard index >= 0 && index < classes.count else { return nil } + return classes[index] } // MARK: CodableWithConfiguration conformance diff --git a/Sources/RolePlayingCore/Player/CreatureType.swift b/Sources/RolePlayingCore/Player/CreatureType.swift new file mode 100644 index 0000000..468c6bf --- /dev/null +++ b/Sources/RolePlayingCore/Player/CreatureType.swift @@ -0,0 +1,24 @@ +// +// CreatureType.swift +// RolePlayingCore +// +// Created by Brian Arnold on 11/8/25. +// + +public struct CreatureType: Sendable { + public let name: String + public let isDefault: Bool? + + private enum CodingKeys: String, CodingKey { + case name + case isDefault = "is default" + } + + init(_ name: String, isDefault: Bool? = nil) { + self.name = name + self.isDefault = isDefault + } +} + +extension CreatureType: Codable { } + diff --git a/Sources/RolePlayingCore/Player/Species.swift b/Sources/RolePlayingCore/Player/Species.swift index 77653a3..5dfc715 100644 --- a/Sources/RolePlayingCore/Player/Species.swift +++ b/Sources/RolePlayingCore/Player/Species.swift @@ -9,34 +9,35 @@ import Foundation /// A collection of species traits, including subspecies. -public class Species: Codable { +public class Species: CodableWithConfiguration { /// Accesses all of the species and subspecies that have been loaded. public var species = [SpeciesTraits]() + public var creatureTypes = [CreatureType]() + + public var defaultCreatureType: CreatureType { + creatureTypes.first(where: { $0.isDefault != nil && $0.isDefault! }) ?? CreatureType("Humanoid") + } + /// Creates a Species instance. public init() { } /// Returns all of the leaf species (species that contain no subspecies). public var leafSpecies: [SpeciesTraits] { - return species.filter { (speciesTraits) -> Bool in - speciesTraits.subspecies.count == 0 - } + return species.filter { $0.subspecies.isEmpty } } /// Returns the species matching the specified name, or nil if not present. - public func find(_ speciesName: String?) -> SpeciesTraits? { - guard speciesName != nil else { return nil } - + public func find(_ speciesName: String) -> SpeciesTraits? { return species.first(where: { $0.name == speciesName }) } - public var count: Int { return species.count } + public var count: Int { species.count } public subscript(index: Int) -> SpeciesTraits? { - get { - return species[index] - } + guard index >= 0 && index < species.count else { return nil } + return species[index] } public func randomElementByIndex(using generator: inout G) -> SpeciesTraits { @@ -45,16 +46,20 @@ public class Species: Codable { enum CodingKeys: String, CodingKey { case species + case creatureTypes = "creature types" } /// Overridden to stitch together subspecies embedded in species. - public required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder, configuration: Configuration) throws { let root = try decoder.container(keyedBy: CodingKeys.self) + let creatureTypes = try root.decodeIfPresent([CreatureType].self, forKey: .creatureTypes) + self.creatureTypes = creatureTypes ?? [] + var leaf = try root.nestedUnkeyedContainer(forKey: .species) var species = [SpeciesTraits]() while (!leaf.isAtEnd) { - let speciesTraits = try leaf.decode(SpeciesTraits.self) + let speciesTraits = try leaf.decode(SpeciesTraits.self, configuration: configuration) species.append(speciesTraits) /// If there are subspecies, append those @@ -65,4 +70,17 @@ public class Species: Codable { self.species = species } + + public func encode(to encoder: any Encoder, configuration: Configuration) throws { + var root = encoder.container(keyedBy: CodingKeys.self) + if !creatureTypes.isEmpty { + try root.encode(creatureTypes, forKey: .creatureTypes) + } + + var leaf = root.nestedUnkeyedContainer(forKey: .species) + let rootSpecies = self.species.filter { $0.parentName == nil } + for speciesTraits in rootSpecies { + try leaf.encode(speciesTraits, configuration: configuration) + } + } } diff --git a/Sources/RolePlayingCore/Player/SpeciesTraits.swift b/Sources/RolePlayingCore/Player/SpeciesTraits.swift index a658182..79ac703 100644 --- a/Sources/RolePlayingCore/Player/SpeciesTraits.swift +++ b/Sources/RolePlayingCore/Player/SpeciesTraits.swift @@ -11,24 +11,6 @@ import Foundation /// Traits representing a species. public struct SpeciesTraits { - public struct CreatureType: Sendable { - public let name: String - - public static let aberration = CreatureType(name: "Aberration") - public static let beast = CreatureType(name: "Beast") - public static let celestial = CreatureType(name: "Celestial") - public static let construct = CreatureType(name: "Construct") - public static let dragon = CreatureType(name: "Dragon") - public static let elemental = CreatureType(name: "Elemental") - public static let fey = CreatureType(name: "Fey") - public static let fiend = CreatureType(name: "Fiend") - public static let giant = CreatureType(name: "Giant") - public static let humanoid = CreatureType(name: "Humanoid") - public static let monstrosity = CreatureType(name: "Monstrosity") - public static let ooze = CreatureType(name: "Ooze") - public static let plant = CreatureType(name: "Plant") - public static let undead = CreatureType(name: "Undead") - } public var name: String public var plural: String public var aliases: [String] @@ -47,7 +29,7 @@ public struct SpeciesTraits { public init(name: String, plural: String, aliases: [String] = [], - creatureType: CreatureType = .humanoid, + creatureType: CreatureType, descriptiveTraits: [String: String] = [:], lifespan: Int, baseSizes: [String] = ["4-7"], @@ -65,7 +47,7 @@ public struct SpeciesTraits { } } -extension SpeciesTraits: Codable { +extension SpeciesTraits: CodableWithConfiguration { private enum CodingKeys: String, CodingKey { case name @@ -80,7 +62,7 @@ extension SpeciesTraits: Codable { case subspecies } - public init(from decoder: Decoder) throws { + public init(from decoder: Decoder, configuration: Configuration) throws { let values = try decoder.container(keyedBy: CodingKeys.self) // Try decoding properties @@ -98,7 +80,7 @@ extension SpeciesTraits: Codable { self.name = name self.plural = plural self.aliases = aliases ?? [] - self.creatureType = CreatureType(name: creatureType ?? CreatureType.humanoid.name) + self.creatureType = creatureType != nil ? CreatureType(creatureType!) : configuration.species.defaultCreatureType self.descriptiveTraits = descriptiveTraits ?? [:] self.lifespan = lifespan self.baseSizes = baseSizes ?? ["4-7"] @@ -108,7 +90,7 @@ extension SpeciesTraits: Codable { // Decode subspecies if var subspecies = try? values.nestedUnkeyedContainer(forKey: .subspecies) { while (!subspecies.isAtEnd) { - var subspeciesTraits = try subspecies.decode(SpeciesTraits.self) + var subspeciesTraits = try subspecies.decode(SpeciesTraits.self, configuration: configuration) subspeciesTraits.blendTraits(from: self) self.subspecies.append(subspeciesTraits) } @@ -138,7 +120,7 @@ extension SpeciesTraits: Codable { } } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder, configuration: Configuration) throws { var values = encoder.container(keyedBy: CodingKeys.self) try values.encode(name, forKey: .name) diff --git a/Tests/RolePlayingCoreTests/PlayerTests.swift b/Tests/RolePlayingCoreTests/PlayerTests.swift index 3698ff4..7898241 100644 --- a/Tests/RolePlayingCoreTests/PlayerTests.swift +++ b/Tests/RolePlayingCoreTests/PlayerTests.swift @@ -147,7 +147,7 @@ struct PlayerTests { "extra languages": 1 } """.data(using: .utf8)! - self.human = try! decoder.decode(SpeciesTraits.self, from: self.humanTraits) + self.human = try! decoder.decode(SpeciesTraits.self, from: self.humanTraits, configuration: configuration) } @Test("Create player with basic traits") diff --git a/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift b/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift index ebc4786..481d3a2 100644 --- a/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift +++ b/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift @@ -13,6 +13,12 @@ import Foundation @Suite("Species Names") struct SpeciesNamesTests { + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } + @Test("Loading and generating species names") func speciesNames() async throws { let bundle = Bundle.module @@ -25,9 +31,9 @@ struct SpeciesNamesTests { // TODO: find a way to test just the minimum functionality. // In the meantime, use the test species. let jsonData = try bundle.loadJSON("TestSpecies") - let species = try decoder.decode(Species.self, from: jsonData) + let species = try decoder.decode(Species.self, from: jsonData, configuration: configuration) let moreJsonData = try bundle.loadJSON("TestMoreSpecies") - let moreSpecies = try decoder.decode(Species.self, from: moreJsonData) + let moreSpecies = try decoder.decode(Species.self, from: moreJsonData, configuration: configuration) let allSpecies = Species() allSpecies.species = species.species + moreSpecies.species diff --git a/Tests/RolePlayingCoreTests/SpeciesTests.swift b/Tests/RolePlayingCoreTests/SpeciesTests.swift index 4187401..04b77e0 100644 --- a/Tests/RolePlayingCoreTests/SpeciesTests.swift +++ b/Tests/RolePlayingCoreTests/SpeciesTests.swift @@ -15,6 +15,11 @@ struct SpeciesTests { let bundle = Bundle.module let decoder = JSONDecoder() + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } @Test("Default initialization creates empty species") func defaultInit() async throws { @@ -25,7 +30,7 @@ struct SpeciesTests { @Test("Load and parse species from JSON file") func species() async throws { let jsonData = try bundle.loadJSON("TestSpecies") - let species = try decoder.decode(Species.self, from: jsonData) + let species = try decoder.decode(Species.self, from: jsonData, configuration: configuration) #expect(species.leafSpecies.count == 8, "all species") #expect(species.count == 11, "all species") @@ -34,13 +39,12 @@ struct SpeciesTests { // Test finding a species by name #expect(species.find("Human") != nil, "Fighter should be non-nil") #expect(species.find("Foo") == nil, "Foo should be nil") - #expect(species.find(nil) == nil, "nil species name should find nil") } @Test("Load uncommon species from JSON file") func uncommonSpecies() async throws { let jsonData = try bundle.loadJSON("TestMoreSpecies") - let species = try decoder.decode(Species.self, from: jsonData) + let species = try decoder.decode(Species.self, from: jsonData, configuration: configuration) // There should be 5 species plus 2 subspecies #expect(species.species.count == 5, "all species") diff --git a/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift b/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift index 2a6fc4a..f7bcc3e 100644 --- a/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift +++ b/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift @@ -14,6 +14,11 @@ import Foundation struct SpeciesTraitsTests { let decoder = JSONDecoder() + let configuration: Configuration + + init() throws { + configuration = try Configuration("TestConfiguration", from: .module) + } @Test("Decode basic species traits") func speciesTraits() async throws { @@ -28,7 +33,7 @@ struct SpeciesTraitsTests { } """.data(using: .utf8)! - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) #expect(speciesTraits.name == "Human", "name") #expect(speciesTraits.plural == "Humans", "plural") @@ -48,7 +53,7 @@ struct SpeciesTraitsTests { } """.data(using: .utf8)! - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) #expect(speciesTraits.name == "Giant Human", "name") #expect(speciesTraits.plural == "Giant Humans", "plural") @@ -69,7 +74,7 @@ struct SpeciesTraitsTests { } """.data(using: .utf8)! - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) #expect(speciesTraits.aliases.count == 1, "aliases count") } @@ -89,7 +94,7 @@ struct SpeciesTraitsTests { let traits = json.data(using: .utf8)! #expect(throws: (any Error).self) { - _ = try decoder.decode(SpeciesTraits.self, from: traits) + _ = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) } } @@ -112,7 +117,7 @@ struct SpeciesTraitsTests { } """.data(using: .utf8)! - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) let subspeciesTraits = try #require(speciesTraits.subspecies.first) #expect(subspeciesTraits.name == "Subhuman", "name") @@ -141,7 +146,7 @@ struct SpeciesTraitsTests { } """.data(using: .utf8)! - let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits) + let speciesTraits = try decoder.decode(SpeciesTraits.self, from: traits, configuration: configuration) let subspeciesTraits = try #require(speciesTraits.subspecies.first) #expect(subspeciesTraits.name == "Folk", "name") @@ -154,18 +159,18 @@ struct SpeciesTraitsTests { @Test("Encode subspecies traits with blending") func encodingSubspeciesTraits() async throws { - let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], descriptiveTraits: [:], lifespan: 90, darkVision: 0, speed: 45) + let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], creatureType: configuration.species.defaultCreatureType, descriptiveTraits: [:], lifespan: 90, darkVision: 0, speed: 45) let encoder = JSONEncoder() // Test 1: Subspecies with blended traits do { var copyOfSpeciesTraits = speciesTraits - var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", lifespan: 45, darkVision: 0, speed: 30) + var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", creatureType: configuration.species.defaultCreatureType, lifespan: 45, darkVision: 0, speed: 30) subspeciesTraits.blendTraits(from: copyOfSpeciesTraits) copyOfSpeciesTraits.subspecies.append(subspeciesTraits) - let encoded = try encoder.encode(copyOfSpeciesTraits) + let encoded = try encoder.encode(copyOfSpeciesTraits, configuration: configuration) let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as! [String: Any] // Confirm species traits @@ -188,10 +193,10 @@ struct SpeciesTraitsTests { // Test 2: Subspecies with different overrides do { var copyOfSpeciesTraits = speciesTraits - let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], descriptiveTraits: ["background": "Something"], lifespan: 45, darkVision: 10, speed: 45) + let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], creatureType: configuration.species.defaultCreatureType, descriptiveTraits: ["background": "Something"], lifespan: 45, darkVision: 10, speed: 45) copyOfSpeciesTraits.subspecies.append(subspeciesTraits) - let encoded = try encoder.encode(copyOfSpeciesTraits) + let encoded = try encoder.encode(copyOfSpeciesTraits, configuration: configuration) let dictionary = try JSONSerialization.jsonObject(with: encoded, options: []) as! [String: Any] // Confirm subspecies traits diff --git a/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json b/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json index 9cd9a1b..8aff22b 100644 --- a/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json @@ -1,4 +1,20 @@ { + "creature types": [ + { "name": "Aberration" }, + { "name": "Beast" }, + { "name": "Celestial" }, + { "name": "Construct" }, + { "name": "Dragon" }, + { "name": "Elemental" }, + { "name": "Fey" }, + { "name": "Fiend" }, + { "name": "Giant" }, + { "name": "Humanoid", "is default": true }, + { "name": "Monstrosity" }, + { "name": "Ooze" }, + { "name": "Plant" }, + { "name": "Undead" } + ], "species": [ { "name": "Dwarf", @@ -63,5 +79,5 @@ "base sizes": ["4-7 ft", "2-4 ft"], "speed": 30 } - ] + ] } From 4585ee3df469dda948fe3165dd0fede41a6a052d Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sun, 9 Nov 2025 08:37:51 -0500 Subject: [PATCH 29/33] Updates to conform to idiomatic Swift. --- .../Player/PlayerDetailView.swift | 15 +++++++-------- .../Player/PlayerListView.swift | 4 ++-- .../RolePlayingCore/Currency/UnitCurrency.swift | 8 ++++++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift index e3f825e..ff5a553 100644 --- a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerDetailView.swift @@ -24,12 +24,11 @@ struct PlayerDetailView: View { ForEach(0.. some View { - let cellIdentifier = characterSheet.cellIdentifiers[indexPath.section][indexPath.item] - let keys = characterSheet.keys[indexPath.section][indexPath.item] - let label = characterSheet.labelKeys[indexPath.section][indexPath.item] + private func traitView(for section: Int, item: Int) -> some View { + let cellIdentifier = characterSheet.cellIdentifiers[section][item] + let keys = characterSheet.keys[section][item] + let label = characterSheet.labelKeys[section][item] switch cellIdentifier { case "labeledText": @@ -198,7 +197,7 @@ struct AbilityItemView: View { } .padding(8) .frame(maxWidth: .infinity) - .background(Color(.tertiarySystemBackground)) + .background(.background.tertiary) .cornerRadius(8) } } diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift index fb4d6c0..a90037a 100644 --- a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Player/PlayerListView.swift @@ -32,11 +32,11 @@ struct PlayerListView: View { } .navigationTitle("Characters") .toolbar { - ToolbarItem(placement: .navigationBarLeading) { + ToolbarItem(placement: .topBarLeading) { EditButton() } - ToolbarItem(placement: .navigationBarTrailing) { + ToolbarItem(placement: .topBarTrailing) { Button { appState.addNewCharacter() if let newPlayer = appState.players[0] { diff --git a/Sources/RolePlayingCore/Currency/UnitCurrency.swift b/Sources/RolePlayingCore/Currency/UnitCurrency.swift index 10ab765..6193a8b 100644 --- a/Sources/RolePlayingCore/Currency/UnitCurrency.swift +++ b/Sources/RolePlayingCore/Currency/UnitCurrency.swift @@ -43,8 +43,12 @@ public final class UnitCurrency : Dimension, @unchecked Sendable { super.init(symbol: symbol, converter: converter) } - // TODO: In order to provide the other init method, I was required to implement - // this one as well. However, I don't know how to reconcile NSCoder with Codable. + // MARK: NSCoding Support + + /// Required initializer for NSCoding support, because `Dimension` conforms to `NSSecureCoding`. + /// Since this class has a custom initializer, Swift requires all designated initializers from the superclass. + /// + /// This library actually uses `Codable` for serialization (see below). public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } From a7eecb6db3dadaadfdc80045f7920c946c374dc6 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sun, 9 Nov 2025 09:21:50 -0500 Subject: [PATCH 30/33] Updated CI/CD to enable code coverage --- .github/workflows/swift.yml | 8 +++++++- README.md | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 350f4ae..de9c41c 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -19,4 +19,10 @@ jobs: - name: Build run: swift build -v - name: Run tests - run: swift test -v + run: swift test --enable-code-coverage + - name: Generate coverage report + run: | + xcrun llvm-cov export -format="lcov" \ + .build/debug/RolePlayingCorePackageTests.xctest/Contents/MacOS/RolePlayingCorePackageTests \ + -instr-profile .build/debug/codecov/default.profdata > coverage.lcov + diff --git a/README.md b/README.md index b5e00f7..eec88fe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# RolePlayingCore [![Build Status](https://travis-ci.org/mrlegowatch/RolePlayingCore.svg?branch=master)](https://travis-ci.org/mrlegowatch/RolePlayingCore) [![codecov.io](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/master/graphs/badge.svg)](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/master) +# RolePlayingCore ![Build Status](https://github.com/mrlegowatch/RolePlayingCore/workflows/Swift/badge.svg) +![Code Coverage](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/main/graph/badge.svg) +![Swift Version](https://img.shields.io/badge/Swift-5.0-orange.svg) +![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20Linux-lightgrey.svg) +[![License](https://img.shields.io/github/license/mrlegowatch/RolePlayingCore)](LICENSE) This framework provides reusable role playing game core logic in the Swift language. It is a work-in-progress. Capabilities will be provided incrementally over time. @@ -6,10 +10,6 @@ The short-term goal for this project is to provide core logic for implementing a The library is built as a generic Swift Package, and the Example character generator app uses the iOS SDK with SwiftUI. The longer-term goal is to leverage this as a framework or library for implementing role playing games and utilities on the desktop and web. -## Requirements - -Xcode 26 or Swift 6 are required. - ## Organization The current organizational groupings include: From d8e156a41e10f1e35cf053183ad2d4df47ba7535 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sun, 9 Nov 2025 09:55:15 -0500 Subject: [PATCH 31/33] Adjustments to CI/CD to support codecov. --- .github/workflows/swift.yml | 8 +++++++- README.md | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index de9c41c..4062d84 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -25,4 +25,10 @@ jobs: xcrun llvm-cov export -format="lcov" \ .build/debug/RolePlayingCorePackageTests.xctest/Contents/MacOS/RolePlayingCorePackageTests \ -instr-profile .build/debug/codecov/default.profdata > coverage.lcov - + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: ./coverage.lcov + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + verbose: true diff --git a/README.md b/README.md index eec88fe..e5c5900 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # RolePlayingCore ![Build Status](https://github.com/mrlegowatch/RolePlayingCore/workflows/Swift/badge.svg) -![Code Coverage](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/main/graph/badge.svg) +![Code Coverage](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/development/graph/badge.svg) ![Swift Version](https://img.shields.io/badge/Swift-5.0-orange.svg) ![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20Linux-lightgrey.svg) [![License](https://img.shields.io/github/license/mrlegowatch/RolePlayingCore)](LICENSE) From 99de8d0ff758f383c91fe3394b06947b264475d9 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sun, 9 Nov 2025 10:11:52 -0500 Subject: [PATCH 32/33] Enabled dead recommended settings and changed Swift compiler version to 6. --- .../CharacterGenerator.xcodeproj/project.pbxproj | 16 +++++++++++++--- README.md | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj index fa5b59d..61242a4 100644 --- a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 2600; - LastUpgradeCheck = 2600; + LastUpgradeCheck = 2610; TargetAttributes = { B6E1AD7C2EB3BC2C00000512 = { CreatedOnToolsVersion = 26.0.1; @@ -312,6 +312,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = VHX6TEH729; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -335,8 +336,10 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -374,6 +377,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = VHX6TEH729; ENABLE_NS_ASSERTIONS = NO; @@ -390,7 +394,9 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 6.0; }; name = Release; }; @@ -401,6 +407,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VHX6TEH729; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -433,7 +440,6 @@ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; XROS_DEPLOYMENT_TARGET = 26.0; }; @@ -446,6 +452,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VHX6TEH729; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -478,7 +485,6 @@ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; XROS_DEPLOYMENT_TARGET = 26.0; }; @@ -490,6 +496,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VHX6TEH729; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; @@ -516,6 +523,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VHX6TEH729; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; @@ -541,6 +549,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VHX6TEH729; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; @@ -566,6 +575,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = VHX6TEH729; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; diff --git a/README.md b/README.md index e5c5900..66b6d57 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RolePlayingCore ![Build Status](https://github.com/mrlegowatch/RolePlayingCore/workflows/Swift/badge.svg) ![Code Coverage](https://codecov.io/gh/mrlegowatch/RolePlayingCore/branch/development/graph/badge.svg) -![Swift Version](https://img.shields.io/badge/Swift-5.0-orange.svg) +![Swift Version](https://img.shields.io/badge/Swift-6.0-orange.svg) ![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20Linux-lightgrey.svg) [![License](https://img.shields.io/github/license/mrlegowatch/RolePlayingCore)](LICENSE) From d5b1ce56d07c10f605f86f7bfb16c0887edbc579 Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sun, 9 Nov 2025 11:42:38 -0500 Subject: [PATCH 33/33] Updates to use dictionaries for fast lookup of frequently accessed elements. --- .../Configuration/Configuration.swift | 4 +- .../RolePlayingCore/Currency/Currencies.swift | 31 ++++++----- .../RolePlayingCore/Player/Backgrounds.swift | 36 +++++++++---- Sources/RolePlayingCore/Player/Player.swift | 4 +- Sources/RolePlayingCore/Player/Skills.swift | 52 +++++++++++++++---- .../BackgroundsTests.swift | 17 +++--- .../RolePlayingCoreTests/CurrencyTests.swift | 24 ++++----- 7 files changed, 105 insertions(+), 63 deletions(-) diff --git a/Sources/RolePlayingCore/Configuration/Configuration.swift b/Sources/RolePlayingCore/Configuration/Configuration.swift index fd0ce95..cec418e 100644 --- a/Sources/RolePlayingCore/Configuration/Configuration.swift +++ b/Sources/RolePlayingCore/Configuration/Configuration.swift @@ -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 { diff --git a/Sources/RolePlayingCore/Currency/Currencies.swift b/Sources/RolePlayingCore/Currency/Currencies.swift index b8b54e2..08a7921 100644 --- a/Sources/RolePlayingCore/Currency/Currencies.swift +++ b/Sources/RolePlayingCore/Currency/Currencies.swift @@ -9,22 +9,25 @@ 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 }) @@ -32,18 +35,14 @@ public struct Currencies { 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) diff --git a/Sources/RolePlayingCore/Player/Backgrounds.swift b/Sources/RolePlayingCore/Player/Backgrounds.swift index 14657ca..bb3729f 100644 --- a/Sources/RolePlayingCore/Player/Backgrounds.swift +++ b/Sources/RolePlayingCore/Player/Backgrounds.swift @@ -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(using generator: inout G) -> BackgroundTraits { - return backgrounds.randomElementByIndex(using: &generator)! + return all.randomElementByIndex(using: &generator)! } // MARK: Codable conformance @@ -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) } } diff --git a/Sources/RolePlayingCore/Player/Player.swift b/Sources/RolePlayingCore/Player/Player.swift index 9d9e34e..7c4ffd8 100644 --- a/Sources/RolePlayingCore/Player/Player.swift +++ b/Sources/RolePlayingCore/Player/Player.swift @@ -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) @@ -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) } diff --git a/Sources/RolePlayingCore/Player/Skills.swift b/Sources/RolePlayingCore/Player/Skills.swift index 88597c2..3c834ef 100644 --- a/Sources/RolePlayingCore/Player/Skills.swift +++ b/Sources/RolePlayingCore/Player/Skills.swift @@ -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 @@ -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) diff --git a/Tests/RolePlayingCoreTests/BackgroundsTests.swift b/Tests/RolePlayingCoreTests/BackgroundsTests.swift index a613575..e4ecb94 100644 --- a/Tests/RolePlayingCoreTests/BackgroundsTests.swift +++ b/Tests/RolePlayingCoreTests/BackgroundsTests.swift @@ -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() @@ -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") } } diff --git a/Tests/RolePlayingCoreTests/CurrencyTests.swift b/Tests/RolePlayingCoreTests/CurrencyTests.swift index 599f8f8..c1091a5 100644 --- a/Tests/RolePlayingCoreTests/CurrencyTests.swift +++ b/Tests/RolePlayingCoreTests/CurrencyTests.swift @@ -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") } @@ -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") @@ -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") @@ -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]