4
\$\begingroup\$

I am working on a game written in Kotlin that has a state management system similar to Redux. The state of the game is kept in a single place and is represented as immutable data classes (no logic). State is mutated by executing "commands" which are functions that perform a discrete transform of the state. Every time the state is mutated the new state is broadcasted via RxKotlin via view models to the UI.

As the state is represented by immutable objects and transformed only via discrete commands, you can time travel through the state to see what it was at any point in time.

Although I wrote it like this as an architectural decision to make the code easier to maintain, it looks like I will also be able to use it to implement a feature of the game.

In the game, the player can travel back in time (undo a turn) by consuming an item they hold. There are two complications with this mechanic:

  1. Mutations of the state are done in small discrete chunks, so a logical game action could be made up of multiple state mutations. For example, buying a weapon consists of two state mutations: paying for the weapon and receiving the weapon. To undo a game action, we need a way to rewind state to a point that makes sense to the player.
  2. In order to time travel, an item must be consumed as a consequence. If the player rewinds state several times in a row, we don't want this item to be returned to the user. In other words: we don't want the consequences of time travel to be reversed by time travel itself!

The way I approched these problems was by:

  1. Grouping state mutatons into logical game actions by giving them a unique tag.
  2. Tagging state mutations that were a consequence of time travel with a unique tag. During time travel, if this unique tag is encountered, increment a counter to keep track of it. After time travel has finished, add the consequences back on.

I wrote an algorithm but I am not happy with it. My software developer senses are tingling, I feel like there must be a nicer way to express this logic.

Ideally, I would like the algorithm to be implemented using functional programming concepts (maybe replacing the loop with recursion or functional primitives like a reducer). This would fit in better with the rest of the project.

I would love it if anyone could give me suggestions to improve this algorithm.

interface IStateManager {
    // Adds a command to the stack which also performs a transformation of the the current game state. 
    fun pushCommand(command: GameCommand<GameState>)
    // Checks what the previous command that was pushed was without modifying any state
    fun peekCommand(): GameCommand<GameState>
    // Returns the previously pushed command and rewinds the game state by one transformation
    fun popCommand(): GameCommand<GameState>
    // Returns true if the state stack has one or more items on it, otherwise false.
    fun canPopCommmand(): Boolean
} 


data class TimeTravelState(
    var currentGroupId: GameCommandGroupingTag?,
    var noConsequences: Int
)

class StateTimeMachine(
    private val stateManager: IStateManager
) {

  fun rewindTime(characterIndexWhoTriggeredTimeTravel: Int) {

    val timeTravelState = TimeTravelState(
        currentGroupId = null,
        noConsequences = 1
    )

    // Repeat while the state/command stack has a length > 0
    while (this.stateManager.canPopCommmand()) {
        // Get the state that was before the last transformation
        val previousCommand = this.stateManager.peekCommand<GameState>()


        if (previousCommand.groupingTag == GameCommandGroupingTag.TimeTravelConsequence) {
            // If the state change was to apply the consequences of time travel, keep track so it can be reapplied
            // after it is popped off the stack
            timeTravelState.noConsequences += 1
        } else if (timeTravelState.currentGroupId == null) {
            // If we haven't stored a group ID yet, store the current one for future comparison
            timeTravelState.currentGroupId = previousCommand.groupingTag
        } else if (timeTravelState.currentGroupId !== previousCommand.groupingTag) {
            // If we have stored a group ID and it isn't the same as the last iteration, we are finished
            break;
        }

        // Pop the latest state off the stack to rewind the state
        this.stateManager.popCommand<GameState>()

    }

    // Apply the accumulated consequences (take the items used to initiate time travel)
    repeat(timeTravelState.noConsequences, {
        val applyTimeTravelConsequencesCommand = ApplyTimeTravelConsequencesCommand(characterIndexWhoTriggeredTimeTravel)
        this.stateManager.pushCommand(applyTimeTravelConsequencesCommand)
    })
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.