From 863ac053c42a5ca0e90edbd018ec2f8b1cb4cd2a Mon Sep 17 00:00:00 2001 From: Celtic Minstrel Date: Thu, 15 Sep 2016 17:25:43 -0400 Subject: [PATCH] Add tests for reading player data from a saved game - Changed cPlayer::equip to a bitset - Use a static constant instead of a loop to initialized player starting spells - Only save spell points if the player has any (current if different from max) - Symbolic forms for trait enum (and save symbolic forms also for skills) - When loading a player, clear data which is not always present in the file - Also add an init test for cPlayer --- src/BoE.xcodeproj/project.pbxproj | 6 +++ src/classes/estreams.cpp | 18 +++++++ src/classes/pc.cpp | 76 +++++++++++++++++----------- src/classes/pc.hpp | 11 ++-- test/files/player/basic.txt | 15 ++++++ test/files/player/skills.txt | 9 ++++ test/files/player/spells.txt | 6 +++ test/init.cpp | 50 ++++++++++++++++++ test/pc_read.cpp | 84 +++++++++++++++++++++++++++++++ 9 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 test/files/player/basic.txt create mode 100644 test/files/player/skills.txt create mode 100644 test/files/player/spells.txt create mode 100644 test/pc_read.cpp diff --git a/src/BoE.xcodeproj/project.pbxproj b/src/BoE.xcodeproj/project.pbxproj index c32b6482..d1fb8851 100755 --- a/src/BoE.xcodeproj/project.pbxproj +++ b/src/BoE.xcodeproj/project.pbxproj @@ -253,6 +253,8 @@ 91BC33921B4388E80008882C /* libboost_thread.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 910D9CA31B36439100414B17 /* libboost_thread.dylib */; }; 91BC33981B4481EF0008882C /* scen.fileio.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91B3EEF20F969BA700BF5B67 /* scen.fileio.cpp */; }; 91BFA3D71901B18F001686E4 /* mask.vert in Copy Shaders */ = {isa = PBXBuildFile; fileRef = 91BFA3D61901B024001686E4 /* mask.vert */; }; + 91C548F81D8B2FE400FE6A7B /* pc_read.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91C548F71D8B2EE400FE6A7B /* pc_read.cpp */; }; + 91C548F91D8B338700FE6A7B /* pc.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 913D05BB0FA1EA0A00184C18 /* pc.cpp */; }; 91C6864A0FD5EEFD000F6D01 /* pc.graphics.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91B3EF0A0F969BD300BF5B67 /* pc.graphics.cpp */; }; 91C749BA1A2D670D008E0E10 /* dialogs in Copy Data Files */ = {isa = PBXBuildFile; fileRef = 91C749B91A2D66F7008E0E10 /* dialogs */; }; 91C763D91B4C50710086D879 /* enums.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 91C763D81B4C4BB30086D879 /* enums.cpp */; }; @@ -738,6 +740,7 @@ 91C2A6EC1B8FA91400346948 /* town_read.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = town_read.cpp; sourceTree = ""; }; 91C2A6ED1B8FA9FB00346948 /* out_read.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = out_read.cpp; sourceTree = ""; }; 91C2A6EE1B8FAA8E00346948 /* talk_read.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = talk_read.cpp; sourceTree = ""; }; + 91C548F71D8B2EE400FE6A7B /* pc_read.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = pc_read.cpp; sourceTree = ""; }; 91C688E60FD702B9000F6D01 /* cursors.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = cursors.hpp; sourceTree = ""; }; 91C688E70FD702B9000F6D01 /* cursors.mac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = cursors.mac.mm; sourceTree = ""; }; 91C749B71A2D6432008E0E10 /* strings */ = {isa = PBXFileReference; lastKnownFileType = folder; path = strings; sourceTree = ""; }; @@ -1360,6 +1363,7 @@ 9176FEC01D550EFC006EF694 /* out_legacy.cpp */, 91C2A6ED1B8FA9FB00346948 /* out_read.cpp */, 91E381491B97678D00F69B81 /* out_write.cpp */, + 91C548F71D8B2EE400FE6A7B /* pc_read.cpp */, 9176FEC11D550EFC006EF694 /* scen_legacy.cpp */, 91CC173A1B421CA0003D9A69 /* scen_read.cpp */, 91CC173B1B421CA0003D9A69 /* scen_write.cpp */, @@ -1904,6 +1908,8 @@ 9176FEC81D550EFE006EF694 /* scen_legacy.cpp in Sources */, 9176FECB1D550EFE006EF694 /* talk_legacy.cpp in Sources */, 9176FECC1D550EFE006EF694 /* town_legacy.cpp in Sources */, + 91C548F81D8B2FE400FE6A7B /* pc_read.cpp in Sources */, + 91C548F91D8B338700FE6A7B /* pc.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/classes/estreams.cpp b/src/classes/estreams.cpp index 477259a2..3af14e9c 100644 --- a/src/classes/estreams.cpp +++ b/src/classes/estreams.cpp @@ -166,6 +166,24 @@ std::istream& operator >> (std::istream& in, eSkill& e){ return in; } +// MARK: eTrait + +cEnumLookup trait_names = { + "tough", "magic-apt", "ambidex", "nimble", "cave-lore", "wood-lore", "const", "alert", + "strong", "regen", "slow", "magic-inept", "frail", "sickly", "bad-back", "pacifist", "anama" +}; + +std::ostream& operator << (std::ostream& out, eTrait e) { + writeEnum(out, e, trait_names, "tough"); + return out; +} + +std::istream& operator >> (std::istream& in, eTrait& e){ + if(!readEnum(in, e, trait_names, eTrait::TOUGHNESS)) + in.setstate(std::ios::failbit); + return in; +} + // MARK: eItemType cEnumLookup item_types = { diff --git a/src/classes/pc.cpp b/src/classes/pc.cpp index e7c230e8..ff69c787 100644 --- a/src/classes/pc.cpp +++ b/src/classes/pc.cpp @@ -23,6 +23,8 @@ extern const std::multiset num_hands_to_use; extern std::map excluding_types; extern short skill_bonus[21]; +// A nice convenient bitset with just the low 30 bits set, for initializing spells +const uint32_t cPlayer::basic_spells = std::numeric_limits::max() >> 2; void cPlayer::import_legacy(legacy::pc_record_type old){ main_status = (eMainStatus) old.main_status; @@ -940,12 +942,10 @@ cPlayer::cPlayer(cParty& party) : party(&party), weap_poisoned(*this) { skill_pts = 65; level = 1; std::fill(items.begin(), items.end(), cItem()); - std::fill(equip.begin(), equip.end(), false); + equip.reset(); - for(short i = 0; i < 62; i++) { - priest_spells[i] = i < 30; - mage_spells[i] = i < 30; - } + priest_spells = basic_spells; + mage_spells = basic_spells; which_graphic = 0; race = eRace::HUMAN; @@ -995,7 +995,7 @@ cPlayer::cPlayer(cParty& party,long key,short slot) : cPlayer(party) { skill_pts = 60; level = 1; std::fill(items.begin(), items.end(), cItem()); - std::fill(equip.begin(), equip.end(), false); + equip.reset(); priest_spells.set(); mage_spells.set(); @@ -1084,13 +1084,9 @@ cPlayer::cPlayer(cParty& party,long key,short slot) : cPlayer(party) { level = 1; std::fill(items.begin(), items.end(), cItem()); - std::fill(equip.begin(), equip.end(), false); + equip.reset(); cur_sp = pc_sp[slot]; max_sp = pc_sp[slot]; - for(short i = 0; i < 62; i++) { - priest_spells[i] = i < 30; - mage_spells[i] = i < 30; - } for(short i = 0; i < 15; i++) { eTrait trait = eTrait(i); traits[trait] = pc_t[slot].count(trait); @@ -1180,14 +1176,16 @@ void cPlayer::writeTo(std::ostream& file) const { file << "UID " << unique_id << '\n'; file << "STATUS -1 " << main_status << '\n'; file << "NAME " << maybe_quote_string(name) << '\n'; - file << "SKILL 19 " << max_health << '\n'; - file << "SKILL 20 " << max_sp << '\n'; + file << "SKILL " << eSkill::MAX_HP << ' ' << max_health << '\n'; + if(max_sp > 0) + file << "SKILL " << eSkill::MAX_SP << ' ' << max_sp << '\n'; for(auto p : skills) { if(p.second > 0) file << "SKILL " << int(p.first) << ' ' << p.second << '\n'; } file << "HEALTH " << cur_health << '\n'; - file << "MANA " << cur_sp << '\n'; + if(cur_sp != max_sp) + file << "MANA " << cur_sp << '\n'; file << "EXPERIENCE " << experience << '\n'; file << "SKILLPTS " << skill_pts << '\n'; file << "LEVEL " << level << '\n'; @@ -1231,7 +1229,17 @@ void cPlayer::readFrom(std::istream& file){ std::string cur; getline(file, cur, '\f'); bin.str(cur); - std::fill(equip.begin(), equip.end(), false); + + // Clear some data that is not always present + equip.reset(); + mage_spells.reset(); + priest_spells.reset(); + weap_poisoned.clear(); + status.clear(); + traits.clear(); + skills.clear(); + cur_sp = max_sp = ap = 0; + while(bin) { // continue as long as no error, such as eof, occurs getline(bin, cur); sin.str(cur); @@ -1244,20 +1252,30 @@ void cPlayer::readFrom(std::istream& file){ }else if(cur == "NAME") name = read_maybe_quoted_string(sin); else if(cur == "SKILL"){ - int i; - sin >> i; - switch(i){ - case -1: - case 20: - sin >> max_sp; - break; - case -2: - case 19: + eSkill skill; + sin >> skill; + switch(skill) { + case eSkill::MAX_HP: sin >> max_health; break; + case eSkill::MAX_SP: + sin >> max_sp; + break; + case eSkill::CUR_HP: + sin >> cur_health; + break; + case eSkill::CUR_SP: + sin >> cur_sp; + break; + case eSkill::CUR_XP: + sin >> experience; + break; + case eSkill::CUR_SKILL: + break; + case eSkill::CUR_LEVEL: + sin >> level; + break; default: - if(i < 0 || i >= 19) break; - eSkill skill = eSkill(i); sin >> skills[skill]; } }else if(cur == "HEALTH") @@ -1291,10 +1309,8 @@ void cPlayer::readFrom(std::istream& file){ sin >> i; priest_spells[i] = true; }else if(cur == "TRAIT"){ - int i; - sin >> i; - if(i < 0 || i > 15) continue; - eTrait trait = eTrait(i); + eTrait trait; + sin >> trait; traits[trait] = true; }else if(cur == "ICON") sin >> which_graphic; diff --git a/src/classes/pc.hpp b/src/classes/pc.hpp index 878072ed..281ec351 100644 --- a/src/classes/pc.hpp +++ b/src/classes/pc.hpp @@ -64,7 +64,10 @@ class cPlayer : public iLiving { cParty* party; template cInvenSlot find_item_matching(Fcn fcn); + static const int INVENTORY_SIZE = 24; public: + // A nice convenient bitset with just the low 30 bits set, for initializing spells + static const uint32_t basic_spells; static void(* give_help)(short,short); eMainStatus main_status; std::string name; @@ -77,8 +80,8 @@ public: unsigned short experience; short skill_pts; short level; - std::array items; - std::array equip; + std::array items; + std::bitset equip; std::bitset<62> priest_spells; std::bitset<62> mage_spells; pic_num_t which_graphic; @@ -90,7 +93,7 @@ public: // transient stuff std::map last_cast; location combat_pos; - short parry; + short parry = 0; iLiving* last_attacked = nullptr; // Note: Currently this is assigned but never read bool is_alive() const; @@ -175,5 +178,7 @@ void operator += (eMainStatus& stat, eMainStatus othr); void operator -= (eMainStatus& stat, eMainStatus othr); std::ostream& operator << (std::ostream& out, eMainStatus e); std::istream& operator >> (std::istream& in, eMainStatus& e); +std::ostream& operator << (std::ostream& out, eTrait e); +std::istream& operator >> (std::istream& in, eTrait& e); #endif diff --git a/test/files/player/basic.txt b/test/files/player/basic.txt new file mode 100644 index 00000000..1d0c7a20 --- /dev/null +++ b/test/files/player/basic.txt @@ -0,0 +1,15 @@ +UID 3 +STATUS -1 alive +NAME "Freddy O'Hara" +SKILL hp 20 +HEALTH 18 +EXPERIENCE 12 +SKILLPTS 3 +LEVEL 2 +STATUS web 3 +TRAIT ambidex +TRAIT nimble +TRAIT bad-back +ICON 3 +RACE human +DIRECTION e diff --git a/test/files/player/skills.txt b/test/files/player/skills.txt new file mode 100644 index 00000000..60410b9a --- /dev/null +++ b/test/files/player/skills.txt @@ -0,0 +1,9 @@ +SKILL str 5 +SKILL dex 6 +SKILL int 3 +SKILL hp 20 +SKILL edged 3 +SKILL traps 2 +SKILL item-lore 1 +SKILL lockpick 2 +SKILL luck 1 diff --git a/test/files/player/spells.txt b/test/files/player/spells.txt new file mode 100644 index 00000000..4c66ec43 --- /dev/null +++ b/test/files/player/spells.txt @@ -0,0 +1,6 @@ +MAGE 3 +MAGE 7 +MAGE 20 +PRIEST 2 +PRIEST 8 +PRIEST 60 \ No newline at end of file diff --git a/test/init.cpp b/test/init.cpp index ddb6c88d..c6c09685 100644 --- a/test/init.cpp +++ b/test/init.cpp @@ -10,6 +10,8 @@ #include "scenario.hpp" #include "creature.hpp" #include "creatlist.hpp" +#include "pc.hpp" +#include "party.hpp" TEST_CASE("Initialization sanity test for terrain") { cTerrain ter; @@ -221,3 +223,51 @@ TEST_CASE("Construction sanity test for monster") { CHECK(pop[0].morale == 800); } } + +TEST_CASE("Construction sanity test for player character") { + cParty party; + cPlayer pc(party); + SECTION("Living base class") { + iLiving& base = pc; + CHECK(base.status.empty()); + CHECK(base.ap == 0); + CHECK(base.direction == DIR_N); + CHECK(base.marked_damage == 0); + } + SECTION("Main player class") { + CHECK(pc.main_status == eMainStatus::ABSENT); + CHECK(pc.name == "\n"); + CHECK(pc.skills.size() == 3); + CHECK(pc.skills[eSkill::STRENGTH] == 1); + CHECK(pc.skills[eSkill::DEXTERITY] == 1); + CHECK(pc.skills[eSkill::INTELLIGENCE] == 1); + CHECK(pc.max_health == 6); + CHECK(pc.cur_health == 6); + CHECK(pc.max_sp == 0); + CHECK(pc.cur_sp == 0); + CHECK(pc.experience == 0); + CHECK(pc.skill_pts == 65); + CHECK(pc.level == 1); + CHECK(pc.items[0].variety == eItemType::NO_ITEM); + CHECK_FALSE(pc.equip.any()); + CHECK(pc.priest_spells == cPlayer::basic_spells); + CHECK(pc.mage_spells == cPlayer::basic_spells); + CHECK(pc.which_graphic == 0); + using weap_slot_t = decltype(pc.weap_poisoned.slot); + CHECK(pc.weap_poisoned.slot == std::numeric_limits::max()); + CHECK(pc.traits.empty()); + CHECK(pc.race == eRace::HUMAN); + // Skip unique_id since it's non-deterministic + CHECK(pc.last_cast.empty()); + CHECK(pc.combat_pos == loc(-1,-1)); + CHECK(pc.parry == 0); + CHECK(pc.last_attacked == nullptr); + } + SECTION("Player spells") { + // This is more just a bitset sanity test + for(int i = 0; i < 62; i++) { + CHECK(pc.priest_spells[i] == (i < 30)); + CHECK(pc.mage_spells[i] == (i < 30)); + } + } +} diff --git a/test/pc_read.cpp b/test/pc_read.cpp new file mode 100644 index 00000000..73395511 --- /dev/null +++ b/test/pc_read.cpp @@ -0,0 +1,84 @@ +// +// pc_read.cpp +// BoE +// +// Created by Celtic Minstrel on 16-09-15. +// +// + +#include +#include "catch.hpp" +#include "pc.hpp" +#include "party.hpp" + +using namespace std; + +TEST_CASE("Loading player character from file") { + ifstream fin; + fin.exceptions(ios::badbit); + cParty party; + cPlayer pc(party); + pc.leave_party(); + // Fill in some junk data + pc.cur_sp = 27; + pc.max_sp = 38; + pc.status[eStatus::CHARM] = 17; + pc.traits[eTrait::ANAMA] = true; + pc.skills[eSkill::LUCK] = 15; + pc.mage_spells = 0xfefefe; + pc.priest_spells = 0xefefef; + pc.weap_poisoned.slot = 2; + pc.ap = 22; + + SECTION("Basic Info") { + fin.open("files/player/basic.txt"); + pc.readFrom(fin); + CHECK(pc.unique_id == 3); + CHECK(pc.main_status == eMainStatus::ALIVE); + CHECK(pc.name == "Freddy O'Hara"); + CHECK(pc.skills.empty()); + CHECK(pc.max_health == 20); + CHECK(pc.max_sp == 0); + CHECK(pc.cur_health == 18); + CHECK(pc.cur_sp == 0); + CHECK(pc.experience == 12); + CHECK(pc.skill_pts == 3); + CHECK(pc.level == 2); + CHECK(pc.status.size() == 1); + CHECK(pc.status[eStatus::WEBS] == 3); + CHECK(pc.traits.size() == 3); + CHECK(pc.traits[eTrait::AMBIDEXTROUS]); + CHECK(pc.traits[eTrait::NIMBLE]); + CHECK(pc.traits[eTrait::BAD_BACK]); + CHECK(pc.which_graphic == 3); + CHECK(pc.race == eRace::HUMAN); + CHECK(pc.direction == DIR_E); + CHECK(pc.mage_spells == 0); + CHECK(pc.priest_spells == 0); + CHECK(pc.weap_poisoned.slot > 24); + CHECK(pc.ap == 0); + } + SECTION("Skills") { + fin.open("files/player/skills.txt"); + pc.readFrom(fin); + CHECK(pc.skills.size() == 8); + CHECK(pc.skills[eSkill::STRENGTH] == 5); + CHECK(pc.skills[eSkill::DEXTERITY] == 6); + CHECK(pc.skills[eSkill::INTELLIGENCE] == 3); + CHECK(pc.skills[eSkill::EDGED_WEAPONS] == 3); + CHECK(pc.skills[eSkill::DISARM_TRAPS] == 2); + CHECK(pc.skills[eSkill::ITEM_LORE] == 1); + CHECK(pc.skills[eSkill::LOCKPICKING] == 2); + CHECK(pc.skills[eSkill::LUCK] == 1); + CHECK(pc.max_health == 20); + } + SECTION("Spells") { + fin.open("files/player/spells.txt"); + pc.readFrom(fin); + // This has bits 3, 7, and 20 set + CHECK(pc.mage_spells == 0x100088); + // This has bits 2, 8, and 60 set + CHECK(pc.priest_spells == 0x1000000000000104); + } +} +