Grumbling about a corner I have painted myself into

I’m writing this because I hope that, in the process of writing it, I will help myself think through how to solve it and also, if I am honest, to have a bit of a whinge.

So I have been trying to implement undo in Rez and boy have I made life difficult for myself.

I’ve introduced an “Undo Manager” that receives a ‘revert’ command every time a game object changes an attribute. When a new event starts it creates a new undo context and the changes flow into that. I could, perhaps, even implement redo although I am less bothered to do that.

All good so far.

Except the view doesn’t update. Oh, yes, Rez has a fairly complex rendering pipeline. This is all Javascript objects and not part of the data model.

This is due to the Game>Scene>Card+>Blocks+ rendering system.

A game has a layout which contains a scene which has a layout that may include one or more cards, which can be flipped to their back side, and each of which may be composed of other cards. It’s very flexible and you can do some cool stuff with it.

For example the stack layout lets you append cards, instead of replacing a single card, as events happen. Only the current card shows its front face and past cards are “flipped”.

So on the front face you might have links representing actions the player can take and, on the flipped face, no links but a statement of what the player did, with the next card representing the result of that action.

Rez also support parameterized links.

<a card="fight" data-monster="goblin">

Passes {monster: "goblin"} to the #fight @card and it can use that parameter as part of its processing.

Again this complicates things because those params have to live somewhere and it can’t be the card itself because it can be used in multiple contexts.

So all this is great, it makes it possible to do nifty stuff. But it comes at a cost.

So far I’ve been unable to either revert or rebuild the view properly after an undo and the view code is sufficiently twisty that I find it hard to hold the whole sequence in my head and think it through to see why.

Alas, neither Claude nor Gemini have made any headway on the problem. In fact their efforts have been pretty hapless. So I am going to have to fix this myself.

</whinge>

2 Likes

I don’t know if this is the same problem, but it could be related. I will describe a problem i had and how i dealt with it;

OK, so all my state (aside from ui settings) is world state. This changes when you do things. Undo and redo work to revert and move the current world state.

So, if you go east then undo, you are no longer east. etc. So far, all good.

I have a graphic stack, which consists of a 2D “scene graph” although it’s not actually a “graph”. Content is arranged in nodes of a parent-child tree. These nodes have position, rotation, scale, alpha, tint (and a few others). The values are also animated.

When you undo, it doesn’t know how to revert the current graphics stack because this information isn’t in the world state.

Sound familiar?

To fix it, i have a convention that the current location (could be a scene for you), is always able to reestablish the graphic stack. This is the graphic analogue of a text system performing look and re-printing the description of the current location.

And, of course, the text part has to be refreshed as well (as would any IF undo system).

So this is all done with my command > x here. But i always factor the bit that draws the scene from the bit that does the text.

So (with added comments);

QUARTERS@ INSIDE   // definition of quarters location
* name
your quarters   
* img it         // definition: refresh stack command
QUARTERSIMG      // establish/reestablish the graphic stack
* x it           // definition: of x command. it will;
> img it         // call `img it` command (see above)
QUARTERSAUDIO    // start any audio
XQUARTERS        // print the text
* n
GOLAB
*+!= go to LAB
GOLAB

Turns out this is extremely useful for doing img here which is sometimes needed to refresh the current graphic stack when a bit of sneaky game logic causes it to change and you don’t want to change the text.

My map locations are now usually auto-generated to these sort of patterns.

2 Likes

Yes the problem is likely a very similar one. I have been able to come up with an approach that I think should work, although at the moment there are still issues.

Note for the future: when copying a view structure for a later undo, it is vital… and I cannot stress this strongly enough, vital that you actually store a copy and not the original object!

Undo implemented :smiley:

1 Like

There’s a popular model of undo handling where you do store the original objects in each undo record, but you also treat all objects as immutable. Every turn creates brand-new objects.

This works well if you can get yourself into the mindset. It’s even reasonably efficient if you’re smart about using shallow object cloning where it makes sense. But it’s not magic. You still have to be very careful about following the model; it’s just being careful in a different place.

(I’ve used this model for a couple of recent projects, like the browser-based blorb editor from a few months ago.)

2 Likes

Yeah.

I did think about it. But I’ve never really liked cloning in OOP languages. I’ve had a merry old time implementing copy semantics for JS objects.

Coming from Clojure & Elixir I seriously considered using immutable data structures for the Rez data model but JS support for immutability isn’t baked in. I felt like I’d be going against the grain.

In practice the Rez data model system works pretty well and most changes boil down to something like:

{
  elemId: "sc_main",
  changeType: "setAttribute",
  attrName: "current_card_id",
  oldValue: "c_intro"
}

I could also store newValue if I ever wanted to implement redo.

However the view hierarchy is a bit more gnarly. In principle I could reduce it to data but there are several levels of nesting and “rehydrating” them was getting finicky. So I opted to clone here instead and implemented a shallow & deep-copy strategy for the view objects.

It helps that I don’t intend undo to cross a save boundary.

I’m not especially happy with any of this. I think in another thread someone mentioned that “Undo” was one of the things you should plan upfront and I dearly wish I’d had that advice when I started :slight_smile:

1 Like

Yes! I’ve coded myself into the exact same corner in the past, so you have my utmost sympathy! Although I do not like everything about React, a thing I found nice about React + Redux is that it almost forced you to setup things in a way where implementing undo is trivial later on. (I still wouldn’t necessarily suggest React for a text-adventure engine, you can achieve the same thing just by planning carefully ahead.)

1 Like

I’m not so familiar with React but I did a lot of work with re-frame and maybe that is conceptually similar?

Because of ClojureScript immutability the whole thing can be expressed as an event loop:

   handle_event(state, event) ->
     new_state = update(state, event)
     render(new_state)
     new_state

   begin_event_loop(initial_state, {})

And an efficient history of state falls out, where it is possible to wind back to any previous version with confidence and events can be replayed to rebuild future state.

I really liked re-frame.

Re-frame is built on top of React, looks like. Yes, same immutable-data-and-undo model as react/redux.

2 Likes