基于文本的冒险游戏的 C++ 文本文件读取和解析

C++ Text file Reading and Parsing for text-based adventure game

在编程方面我有点菜鸟,但作为一个小项目,我决定尝试制作一个非常简单的基于文本的冒险游戏,只是为了一些乐趣和练习。我不知道这些类型的游戏通常是如何制作的,但我决定制作一个包含所有实际文本的文本文件,而不是在代码中键入所有内容,所以这就是我的 "gamelocationdata.txt" 文件当前的样子。

[castleStart]
{
=castleStart=
You find yourself in a dark room inside a castle.
The walls are made of mossy stone and the entire room has a very eerie atmosphere.

There is a green bottle on the floor.
There are 2 exits, east and south.
Both exits are large doorways with grand arches.
You can see a large room through the south exit, but the east exit looks very dark and
somewhat frightening.

What will you do?
#"look arch" castleStartLookArch
#"look wall" castleStartLookWall
#"look bottle" castleStartLookBottle itemcond: hasBottle "0"
#"take bottle" castleStartLookBottle itemcond: hasBottle "0"
#"pick bottle" castleStartLookBottle itemcond: hasBottle "0"
#"go south" castleHall
#"go east" castleDark loccond: hasBeenCastleDark "0"
#"wait" castleStartWait
}

[castleStartLookArch]
{
=castleStart=
The arches above the doors look very fancy.
You can't quite figure out why this tiny room deserves to be decorated as such.
#(castleStart)
}

[castleStartLookWall]
{
=castleStart=
The wall is made of stone masonry in an old-fashioned way. 
Some stones are covered in moss and there are cobwebs in the corners.
#(castleStart)
}

[castleStartWait]
{
=castleStart=
You sit still and admire the wall for a while.
Nothing happens.
#(castleStart)
}

[castleStartLookBottle]
{
=castleStart=
You pick the bottle up. It is covered in cobwebs.
The bottle is green and the label reads "1337". It contains a purple liquid.

Do you want to put the bottle in your backpack?
#"yes" castleStartTakeBottle
#"no" castleStartNoTakeBottle
}

[castleStartTakeBottle]
{
=castleStart=
You take the bottle and put it in your backpack.
+item: Bottle1337
+itemcond: hasBottle "1"
#(castleStart)
}

[castleStartNoTakeBottle]
{
=castleStart=
You put the bottle back down again.
#(castleStart)
}

[useBottle1337]
{
=curLocation=
You open the bottle and drink its contents.
It tastes very sweet.

You suddenly feel slightly stronger and more powerful.
+strength: 5
+remove_item: Bottle1337
#(castleStart)
}

[castleHall]
{
=castleHall=
You walk though the southern doorway and enter the grand hall of the castle.
It seems like the entire castle is just as old and worn out as the walls in that room,
though still very nicely decorated.
There are only a few candles on the walls, and they are somehow lit despite
the castle seeming very empty. There is not not a person to be seen.

You can go back north or proceed south through the hall.
#(castleStart)
}

[castleDark]
{
=castleStart=
You slowly tread into the dark room to the east, looking
around you as your surroundings get darker and darker.
Suddenly, you hear a noise. It sounds like the growling of an angry dog!
Horrified, you hastily turn around and run back.
+loccond: hasBeenCastleDark "1"
#(castleStart)
}

我意识到我可能吃多了不能嚼,但这就是我编写的格式应该如何工作:

Example: [castleStart] is the name of a "location", and the curly braces that come after encapsulate everything that has to do with that location.

Example: =castleStart= is the location to print for the player when they ask where they currently are.

The stuff that comes after that is what will be printed on screen when the player "enters" that location.

After the location text, there are a bunch of options that all start with a "#".

Example: #"wait" castleStartWait If the player types "wait", they will be taken to the "location" named [castleStartWait].

Example: #"look bottle" castleStartLookBottle itemcond: hasBottle "0" If the player types "look bottle", they will be taken to the location named [castleStartLookBottle] as long as they meet the "item requirement" that they do not already have the bottle.

Example: #"go east" castleDark loccond: hasBeenCastleDark "0" If the player types "go east", they will be taken to the location named [castleDark] as long as they meet the "location requirement" that they haven't already been there.

Example: #(castleStart) This will use the same options as the ones listed in [castleStart].

Example: +strength: 5 This should add 5 points to the player's "strength" stat when they enter the location and print some hardcoded message like "You have acquired 5 strength points!"

现在,问题来了:如何编写读取和解析特定位置数据并将其存储在特定std::strings中的函数?

例如,如果我这样做

readAndParseLocationData( castleStart );

它应该在文本文件中查找 [castleStart],然后读取等号 (=castleStart=) 之间的内容并将其存储在 "std::string printLoc" 中,然后读取后面的文本并将其存储在 "std::string locText"等等。

这是我目前所有的代码:

//main.cpp
#include <iostream>
#include <string>
#include "ClearScreen.h"
#include "LocationData.h"

int main()
{
    ClearScreen();
    std::cout << "I am a banana!\n\n"; // this is just a test

    readAndParseLocationData( "castleHall" );
    printLocationData( "castleStart" ); // this is supposed to be used for debugging the location data by printing it.

    return 0;
}

--

//LocationData.cpp
#include "LocationData.h"
#include <fstream>
#include <iostream>
#include <string>

void readAndParseLocationData( std::string location )
{
    location.insert( 0,"[" );
    location.append( "]" );
    std::ifstream locfile( "gamelocationdata.txt" );
    if( locfile.is_open() )
    {
        std::string line;
        bool foundFile = false;
        for( unsigned int curLine = 0; getline( locfile,line ); curLine++ )
        {
            if( line.find( location ) != std::string::npos )
            {
                std::cout << "found: " << location << ", line: " << curLine << "\n";
                foundFile = true;
            }
        }
        if( !foundFile )
        {
            std::cout << "\nERROR: Location " << location << " not found in data file!\n";
        }
        locfile.close();
    }
    else
    {
        std::cout << "\nERROR: Unable to open location data file!\n";
    }
}
void printLocationData( std::string location )
{
    //TODO: Implement
}

我所做的一切(通过广泛的谷歌搜索)是​​查找位置名称并将它所在的行打印到控制台。

我在 Windows 7 上使用 Visual Studio Express 2013。

我也很想知道是否有任何方法可以总体上改进我的代码或格式!

您似乎想要做的是在遇到您希望访问的位置时及时解析文件。如果您希望您的游戏变得太大而无法解析一次并存储在内存中,这是一个好主意,但对于像这样的小示例,它可能是不必要的。

但是,如果您希望出于学习目的实施此功能,则需要研究一些事项。首先,您当前的想法是每次要查看某个位置时都重新解析文件。更好的想法是为位置数据设计某种表示形式并实施某种缓存。一种可能的方法(仅考虑名称和位置文本)是:

class Location
{
private:
    std::string name;
    std::string displayName;
    std::string locationText;
}

std::unordered_map<std::string, Location> cache;

Location& readAndParseLocationData( std::string location )
{
    //if we have already parsed this location
    if (cache.count(location))
    {
        return cache[location];
    }
    else
    {
        Location parsed_location{};
        //do the parsing
        cache[location] = parsed_location;
    }
}

为了实际进行解析,我会写一个简单的 recursive descent parser。对于您的情况,它看起来像这样(伪代码):

Until we have found the location:
    Look for a location
    If this location matches:
        Read the location into a string
        Look for a opening brace
        Read the location display name into a string
        Read the rest into a string up until a closing brace

对于偶然发现这个 2 年前的问题的未来读者,这是我最终用来解析位置数据的最终代码。

(它很旧而且无论如何都不漂亮,但请注意,我的主要问题的解决方案是学习 std::getline 和 std::string 的各种操作方法 - 主要是 find() 和 substr ().)

struct Location final
{
    std::string displayName;
    std::string optionsName;
    std::string locationText;
    std::string defaultOption;
    std::string shop;
    std::vector<int> battleLevel;
    std::vector<int> battleChanceLevel;
    std::vector<int> battleChancePercentage;
    std::vector<std::string> battleEnemy;
    std::vector<std::string> battleChanceEnemy;
    std::vector<std::string> addItems;
    std::vector<std::string> addWeapons;
    std::vector<std::string> addConds;
    std::vector<std::string> addStats;
    std::vector<std::string> removeItems;
    std::vector<std::string> removeWeapons;
    std::vector<std::string> removeConds;
    std::vector<std::string> removeStats;
    std::unordered_set<std::string> options;
    std::unordered_map<std::string, std::string> resultLoc;
    std::unordered_map<std::string, std::string> conds;
};

std::unordered_map<std::string, Location> locationCache;
std::unordered_map<std::string, std::string> locationNewFileCache;

Location& loadLocationData( const std::string& location, const std::string& filename, const bool overRideData, const bool dataFile )
{
    if( ::locationCache.count( location ) && overRideData == false ) // If we already parsed this location...
        return ::locationCache[ location ]; // Return cached data.
    else
    {
        auto filePath = std::string( "game/data/" );
        if( !dataFile )
            filePath.append( "locations/" );
        std::ifstream locFile( filePath + filename );
        if( locFile.is_open() )
        {
            bool foundLoc = false;
            for( std::string line; std::getline(locFile, line); )
            {
                Location parsedLocation;
                if( line.find( "[" + location + "]" ) != std::string::npos )
                {
                    foundLoc = true;

                    // Parse optionsname/next filename.
                    std::string optsName; std::getline( locFile, optsName );
                    if( optsName.find( ".txt" ) != std::string::npos )
                        ::locationNewFileCache[ location ] = optsName;
                    else
                        parsedLocation.optionsName = optsName;

                    // Parse display name.
                    std::string dispName; std::getline( locFile, dispName );
                    parsedLocation.displayName = dispName;

                    // Parse location text.
                    std::string locText;
                    for( std::string scanLine; ( (scanLine.empty()) ? true : scanLine.back() != '}' ) && std::getline( locFile, scanLine ); )
                    {
                        if( !scanLine.empty() && scanLine.front() == '{' )
                            locText = scanLine;
                        else
                            locText.append( "\n" + scanLine );
                    }
                    if( locText.size() >= 2 )
                    {
                        locText.erase( locText.begin() ); // Remove { at beginning.
                        locText.pop_back(); // Remove } at end.
                    }
                    parsedLocation.locationText = locText;

                    // Parse rest.
                    bool endReached = false;
                    for( std::string scanLine; !endReached && std::getline( locFile, scanLine ); )
                    {
                        if( !scanLine.empty() )
                        {
                            switch( scanLine.front() )
                            {
                            case '*': endReached = true; break; // END
                            case '#': // OPTION / DEFAULT OPTION
                                if( scanLine.at( 1 ) == '"' ) // OPTION
                                {
                                    scanLine.erase( 0, 2 );
                                    auto optionName = scanLine.substr( 0, scanLine.find( '\"' ) );
                                    parsedLocation.options.insert( optionName );
                                    scanLine.erase( 0, scanLine.find( '\"' ) + 2 );

                                    auto optionResultLoc = scanLine.substr( 0, scanLine.find( ';' ) );
                                    parsedLocation.resultLoc[ optionName ] = optionResultLoc;
                                    scanLine.erase( 0, scanLine.find( ';' ) + 1 );

                                    if( !scanLine.empty() ) // if the option has conditions...
                                    {
                                        auto condType = scanLine.substr( 0, scanLine.find( ':' ) );
                                        scanLine.erase( 0, scanLine.find( ':' ) + 2 );
                                        if( condType == "loccond" || condType == "itemcond" || condType == "statcond" )
                                            parsedLocation.conds[ optionName ] = scanLine;
                                        else
                                            logError( "Invalid location data syntax in " + filename + "!", "Invalid condition" );
                                    }
                                }
                                else if( scanLine.at( 1 ) == '(' ) // DEFAULT OPTION
                                {
                                    scanLine.erase( 0, 2 );
                                    parsedLocation.defaultOption = scanLine.substr( 0, scanLine.find( ')' ) );
                                }
                                else
                                    logError( "Invalid location data syntax in " + filename + "!", "Neither option nor default option specified" );
                                break;
                            case '+': // ACTION (ITEM / STAT / CONDITION)
                                scanLine.erase( scanLine.begin() );
                                auto action = scanLine.substr( 0, scanLine.find( ':' ) );
                                scanLine.erase( 0, scanLine.find( ':' ) + 2 );
                                auto value = scanLine;

                                if( action == "item" )
                                    parsedLocation.addItems.push_back( value );
                                else if( action == "remove_item" )
                                    parsedLocation.removeItems.push_back( value );
                                else if( action == "weapon" )
                                    parsedLocation.addWeapons.push_back( value );
                                else if( action == "remove_weapon" )
                                    parsedLocation.removeWeapons.push_back( value );
                                else if( action == "itemcond" || action == "loccond" )
                                    parsedLocation.addConds.push_back( value );
                                else if( action == "battle" )
                                {
                                    auto enemyName = value.substr( 0, value.find( ' ' ) );
                                    value.erase( 0, value.find( ' ' ) + 1 );
                                    auto nEnemies = std::stoi( value.substr( 0, value.find( ',' ) ) );
                                    value.erase( 0, value.find( ',' ) + 1 );
                                    auto level = std::stoi( value );
                                    for( auto i = 0; i < nEnemies; ++i )
                                    {
                                        parsedLocation.battleEnemy.push_back( enemyName );
                                        parsedLocation.battleLevel.push_back( level );
                                    }
                                }
                                else if( action == "battlechance" )
                                {
                                    auto enemyName = value.substr( 0, value.find( ' ' ) );
                                    value.erase( 0, value.find( ' ' ) + 1 );
                                    auto chance = std::stoi( value.substr( 0, value.find( ',' ) ) );
                                    value.erase( 0, value.find( ',' ) + 1 );
                                    auto level = std::stoi( value );

                                    parsedLocation.battleChanceEnemy.push_back( enemyName );
                                    parsedLocation.battleChancePercentage.push_back( chance );
                                    parsedLocation.battleChanceLevel.push_back( level );
                                }
                                else if( action == "shop" )
                                    parsedLocation.shop = value;
                                else
                                    parsedLocation.addStats.push_back( action + " " + value ); // Assume it's a stat.
                                break;
                            }
                        }
                    }
                    ::locationCache[ location ] = parsedLocation;
                    return ::locationCache[ location ];
                }
            }
        }
        else
            logError( "Unable to open location data file " + filename + "!" );
    }
    static Location dummyLocation;
    return dummyLocation;
}