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
This commit is contained in:
2016-09-15 17:25:43 -04:00
parent 8aaa0a24c0
commit 863ac053c4
9 changed files with 242 additions and 33 deletions

View File

@@ -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 = "<group>"; };
91C2A6ED1B8FA9FB00346948 /* out_read.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = out_read.cpp; sourceTree = "<group>"; };
91C2A6EE1B8FAA8E00346948 /* talk_read.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = talk_read.cpp; sourceTree = "<group>"; };
91C548F71D8B2EE400FE6A7B /* pc_read.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = pc_read.cpp; sourceTree = "<group>"; };
91C688E60FD702B9000F6D01 /* cursors.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = cursors.hpp; sourceTree = "<group>"; };
91C688E70FD702B9000F6D01 /* cursors.mac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = cursors.mac.mm; sourceTree = "<group>"; };
91C749B71A2D6432008E0E10 /* strings */ = {isa = PBXFileReference; lastKnownFileType = folder; path = strings; sourceTree = "<group>"; };
@@ -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;
};

View File

@@ -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 = {

View File

@@ -23,6 +23,8 @@ extern const std::multiset<eItemType> num_hands_to_use;
extern std::map<const eItemType, const short> 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<uint32_t>::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;

View File

@@ -64,7 +64,10 @@ class cPlayer : public iLiving {
cParty* party;
template<typename Fcn>
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<cItem,24> items;
std::array<bool,24> equip;
std::array<cItem,INVENTORY_SIZE> items;
std::bitset<INVENTORY_SIZE> 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<eSkill,eSpell> 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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,6 @@
MAGE 3
MAGE 7
MAGE 20
PRIEST 2
PRIEST 8
PRIEST 60

View File

@@ -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<weap_slot_t>::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));
}
}
}

84
test/pc_read.cpp Normal file
View File

@@ -0,0 +1,84 @@
//
// pc_read.cpp
// BoE
//
// Created by Celtic Minstrel on 16-09-15.
//
//
#include <fstream>
#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);
}
}