views:

51

answers:

3

Hey all,

I have a state machine design that needs to support playback. We have states that perform actions and sometimes need to generate random numbers. In case the program shuts down while in the middle of the FSM's execution, the program needs to playback the whole FSM using the same random numbers as before.

For a basic example, let's say I had three states: A, B, and C. The FSM will call a state's Execute() function. At the end of the function, the state will post an event, and the FSM will determine which state to go to next. In state A, it will call rand(). If the number is even, it will post an event to go to state B, otherwise state C should be the next state.

void StateA::Execute(IEventQueue& rQueue)
{
    int num = rand();
    if( num % 2 == 0 )
    {
        rQueue.PostEvent("GoToStateB");
    }
    else
    {
        rQueue.PostEvent("GoToStateC");
    }
}

If the random number is 69, then it should go to state C. While in state C, it's possible that the program might quit. When the program starts up again, it should playback the state machine. Obviously, for this to work correctly, it can't generate a completely new random number, it needs to use 69 again for accurate playback.

I have a file stream interface that I can use for saving data to a file, but the code is a little ugly:

void StateA::Execute(IEventQueue& rQueue, IFileStream& rStream)
{

    int num = 0;

    // fails if there's no more data to read
    bool bSuccess = rStream.ReadInt(num);
    if (!bSucess)
    {
        num = rand();
        rStream.WriteInt(num);
    }

    // same code as before
}

My only problem with this solution is that I don't care for having to check the stream for data first and then conditionally write to the same stream.

I thought about hiding it like this:

void StateA::Execute(IEventQueue& rQueue, IStream& rStream)
{

    int num = 0;

    num = rand();
    rStream & num;

    // same code as before
}

Inside IStream, operator& (probably not the best use of overloading) would actually try to read an int from the stream. If that stream was empty, it would then write it instead. Like before, the behavior would be: read first until the end of stream, and then start appending.

So I guess my question is: is there a common idiom for this type of playback that I might be overlooking? Does anyone have alternate suggestions? I feel like I'm starting to over-complicate the design a bit.

Thanks!

A: 

I suspect you should have two files: one that records the events you are playing, and the other that you read "re-play" events from. If the re-play file is longer than the "recording" file, then that is the one you use for a re-play.

I also would not use operator overloading as you suggested. Perhaps just use a ternary operator.

Brent Arias
A: 

I'm not sure I understand the rationale behind "playback", but can't you simply wrap the whole "random-number or read-from-file" logic behind a class or function?

UPDATE

On the subject of "playback" and your design in general, I'm not sure it's normal for a FSM to generate its own stimulus (i.e. the random numbers which in turn trigger state transitions). Normally, the stimulus is provided externally. If you re-factor with this in mind, then you no longer have this messy problem!

Oli Charlesworth
Could you elaborate on what you mean by it being provided externally?
lhumongous
@lhumongous: Normally, one has input->FSM->output, where the input is the stimulus that triggers the FSM to change state. In your design, there doesn't seem to be any input (although I appreciate you've probably provided a cut-down example). Nevertheless, I do question what it means for a state to be providing its own stimulus.
Oli Charlesworth
This is an event driven FSM, the events can be either internal or external. This is pretty common design that I've run across. It's frequently used when designing FSMs for AI agents.
lhumongous
But other than random inputs, what other sources of internal stimulus are there? i.e. is your FSM deterministic apart from the use of RNGs?
Oli Charlesworth
+1  A: 

Why have the states interact directly with the filestream? Single Responsibility says we should have a class who's job it is to provide the proper number based on some logic.

struct INumberSource {
    virtual int GenNextNumber() = 0;
}

// My job is to provide numbers from an RNG
struct RNGNumberSource : public INumberSource {
    virtual int GenNextNumber() {
        return rand();
    }
}

// My job is to write any numbers sourced through me to a file
// I delegate to another source to get an actual number
class FileStreamTrackingNumberSource : INumberSource {
public:
    FileStreamTrackingNumberSource(INumberSource& source, IFileStream& stream)
        : altSource(source), fileStream(stream) { }

    virtual int GenNextNumber() {
        int num = altSource.GenNextNumber();
        fileStream.WriteInt(num);
        return num;
    }
private:
    INumberSource altSource;
    IFileStream& fileStream;
}

// My job is to source numbers from a file stream delegating to an
// alternate source when I run out
class FileStreamNumberSource : public INumberSource {
public:
    FileStreamNumberSource(INumberSource& source, IFileStream& stream)
        : altSource(source), fileStream(stream), failedRead(false) { }

    virtual int GenNextNumber() {
        int num = 0;

        if(failedRead || !(failedRead = fileStream.ReadInt(num))) {
            num = altSource.GenNextNumber();
        }

        return num;
    }

private:
    INumberSource& altSource;
    IFileStream& fileStream;
    bool failedRead;
}

So in your case you would provide an IFileStream and RNGNumberSource to a FileStreamTrackingNumberSource and provide that and the same IFileStream to a FileStreamNumberSource. That FileStreamNumberSource is what you would give to your State's INumberSource parameter.

Assuming you only needed the number to choose the next state then your state code could look like this:

void StateA::Execute(IEventQueue& rQueue, INumberSource& numberSource)
{
    if( numberSource.GenNextNumber() % 2 == 0 )
    {
        rQueue.PostEvent("GoToStateB");
    }
    else
    {
        rQueue.PostEvent("GoToStateC");
    }
}
joshperry
Pretty slick! I like this. Interacting directly with the file stream was undesirable. Trying to properly abstract that away was stumping me. This gives me something to think about. Thanks for the feedback.
lhumongous