Categories
Game Design Game Development Geek / Technical

Toytles: Leaf Raking Progress Report – Streamlining and Planning

In the late report from last week, I went into detail about some changes I made to improve the quality of the codebase, then ended by saying that I wasn’t going to do it again unless it was related to a feature in development. That is, no cleaning up for the sake of cleaning up, as it is premature and potentially wasteful when there is more pressing work to do.

In fact, both stories meant to be completed in sprint 5 had to move to sprint 6.

Sprint 6: More personality injections; streamline waiting for rain

Last week’s sprint plan was:

  • Give each neighbor unique text as unhappy clients
  • Streamline “Wait for Rain” option

I did 7 hours of game development, which is amazing considering I was on vacation for part of this week and did more hours of work than most of the weeks since I started working on these updates.

I finished streamlining the “Wait for Rain” option. It took only 30 minutes, and yet I think it makes a big difference.

Before, if you tried to rake leaves while it was raining, you would get a message informing you that the rain was preventing you from working and asking if you wanted to wait it out.

Your options were to wait 10 minutes or to wait an hour.

Light rain is like a drizzle, and every 10 minutes, which is the smallest unit of time in the game, there is a chance that the rain will start or stop. So it makes sense to want to wait 10 minutes in this case.

But heavier rain will rain for the full hour, so you’ll want to wait that long.

If you were at the top of the hour, this is fine. But the problem is that you could try to rake at any time within an hour, so waiting an hour means that you lose out on anywhere from 10 to 50 minutes depending on when you were trying to start.

The original solution was tedious and frankly non-obvious as an optimal move: just wait 10 minutes over and over until you get to the top of the hour.

Now, you get presented with waiting for 10 minutes and waiting until the end of the hour.

Wait for Rain options

It’s what you would want to do anyway, so now the option is there and easier to execute.

More code changes

Ok, so that was half an hour. What was I doing for the remainder of the time?

Well, I didn’t get the unhappy client text in, but I did a bunch of work to make it possible.

So wait, did I make a bunch of irrelevant code changes again?

No. This time the code changes were paving the way for this feature and not just random opportunities to make my inner technician happy.

Here’s your warning that the rest of this post will get kind of technical!

Currently, when you visit a neighbor, they have one unique thing to say to you which is based upon whether they are a prospective client, a current client, or an ex-client.

I knew the current implementation would be limiting. It’s literally a map of neighbor IDs to a map of client statuses to pieces of text.

typedef std::map<int, std::string> NeighborStatusToGreeting;

It’s a bit unwieldy, but it works for now. When it comes to adding new dialog options based on other things, such as the happiness of the client or the time of day or which date it is, well, I knew this solution wouldn’t have a long life in this codebase.

So I spent a majority of the time planning and researching a better solution. I didn’t need it to do everything possible, but I did need to make something more flexible for my immediate need.

At a high level, I need to take some criteria, then filter out any dialog text that wouldn’t match that criteria.

The current implementation is just hardcoded to pay attention to the visited neighbor’s client status, and so I need to abstract it a bit to make it handle other pieces of data.

In this case, I need to know if the client is unhappy, which isn’t actually a piece of state in the Neighbor struct. It’s related to whether or not the the yard is dangerously close to being overfilled with leaves in the case of a night of incredibly heavy leaf fall.

That is, if you are potentially one turn away from losing the client, the client is nervous, and they should say something about it when you visit them rather than pretend nothing is wrong. Which means that I need to know not only whether the neighbor is a client or not but also the amount of leaves in their yard to determine which dialog text to present to the player. Or more generally, I need more data to act as criteria to validate in order to create a filter that gives me the text I want and leaves out the text I don’t want.

I ended up moving a bunch of code into separate files, partly to make it easier to test drive my new code. As I said before, I wish my Past Self had done more of this quality work, and I don’t want to let Future Self down the way I’ve been let down.

Then I created what I called Validators, and so far I have:

    struct Validator
    {
        virtual bool validate(const ValidatorArgs & args) const = 0;
    };

    struct IsProspect : public Validator
    {
        virtual bool validate(const ValidatorArgs & args) const;
    };

    struct IsClient: public Validator
    {
        virtual bool validate(const ValidatorArgs & args) const;
    };

    struct IsExClient: public Validator
    {
        virtual bool validate(const ValidatorArgs & args) const;
    };

It should be clear that each one will return true or false based on whether or not a neighbor is a prospect, a client, or an ex-client.

Initially, each validator took a Neighbor object as a parameter, since that object had a clientStatus which was the only piece of data I needed.

In preparation for the next validator, IsUnhappyClient, I knew I needed more data to provide to it, but it felt wrong to append more arguments to the validate() function, especially if it ends up being irrelevant for most functions.

So I created ValidatorArgs, which will take all the arguments needed, and each validator can look at the piece that is relevant to it. It means that each time I add new data to be used in future validators, I don’t need to change the signature of all of the validate() functions.

For the above code, this implementation will suffice:

 struct ValidatorArgs
    {
        Neighbor * neighbor;
    };

As an example, here’s IsProspect’s validate():

bool IsProspect::validate(const ValidatorArgs & args) const
{
    return PROSPECT_STATUS == args.neighbor->clientStatus;
}

And if you are wondering what one unit test looks like:

class NeighborDialogCriteriaFixture : public Test
{
    public:  
        NeighborDialogCriteriaFixture() {}
        ~NeighborDialogCriteriaFixture() {}
    
    public: 
        NeighborDialogCriteria::ValidatorArgs args; 
};
    
TEST_F(NeighborDialogCriteriaFixture, CanValidateIfNeighborIsAProspect)
{
    Neighbor prospect(MR_CARDINAL, PROSPECT_STATUS, 0, 5, 0);
    args.neighbor = &prospect;
    EXPECT_TRUE(NeighborDialogCriteria::IsProspect().validate(args));
}

So I create a prospect, then pass it into validate() as part of ValidatorArgs, then test that it does in fact identify this neighbor as a prospect.

There’s also a couple of negative tests that check that it won’t validate if the neighbor is a client and if the neighbor is an ex-client.

What’s next? Well, I didn’t get to it yet, but the plan is to add a new member to ValidatorArgs representing yard data, which will be used in the IsUnhappyClient validator.

And ultimately, all of these validators will get used to determine which filters I should use to identify the appropriate dialog text.

So I envision each greeting text being tagged in some way, such as by client status, and those validators being used to create a list of tags to filter on, then one piece of text is selected and presented in the dialog box.

This also sets up the possibility of having more than one piece of text for a given criteria, so when you visit a neighbor, you’ll get a different greeting each day.

Normally I would love to keep things simple until they need to be more complicated. This solution feels like it is getting more complicated than I wanted it to be at this point, but I wasn’t sure how to take a smaller step to accomplish what I am setting out to do.

The downside is that this work wasn’t something I identified needing to do this sprint, and it meant that I didn’t get the grumpy text into the game yet.

Validating Personality

This coming week, I plan to finish this validator/filter work, and I expect it will open the way for me to add lots of greetings for each neighbor.

In future sprints, I’d like to add some graphical enhancements, including dynamic facial expressions. If a neighbor is greeting you grumpily, I want you to see that they are not smiling.

I’ve also been toying with the idea of enhancing the greetings so they are more like actual back-and-forth dialog with the player. That is, perhaps the player gets asked a question, then has different answers as options, and each one takes you to a different branch of dialog. Perhaps entire transactions can occur, such as giving or receiving gifts, or getting tagged in a way that opens up or closes off certain dialog options with other neighbors.

But, let’s not get ahead of ourselves. I still need to get grumpy neighbor text into the game.

Thanks for following along!

Toytles: Leaf Raking Player's Guide

Want to learn when I release updates to Toytles: Leaf Raking or about future games I am creating? Sign up for the GBGames Curiosities newsletter, and get the 24-page, full color PDF of the Toytles: Leaf Raking Player’s Guide for free!

One reply on “Toytles: Leaf Raking Progress Report – Streamlining and Planning”

Comments are closed.