Tic-Tac-Toe Example (Python extension and state machine animations)

I’m going to teach a multi-day beginner TouchDesigner workshop in a few days. Towards the end, I want to give an impression of the power of Python extensions and parent shortcuts. So in preparation for this, I made Tic Tac Toe. I’d like to get feedback from anyone on what you think of the implementation. Maybe try implementing it yourself before looking at what I did? If you do, try to implement these features:

  • You play by clicking in a 3x3 grid. Clicking instantly adds the correct letter to the empty cell.
  • When the game is over, a game over screen fades in saying who won or if it was a tie.
  • Clicking on the game over screen restarts the game.
  • When playing, there’s a button for instructions that fades in an instructions screen. Clicking it instantly dismisses it.
  • When playing, there’s an indicator saying whose turn it is.

Secondary to this Tic Tac Toe demo, I tried implementing Model-View-Controller with a Python extension but keeping the state entirely in a timer CHOP (using segments). This was a throwback to how I learned MVC from iOS programming: UIViewController | Apple Developer Documentation What do you think of this design? I thought about merging this MVC design into the TicTacToe example, but it seems overkill.
tic_tac_toe_example.tox (110.3 KB)

Hi @DavidBraun,

very interesting to dive into. Because of recent conversations just also wanted to bring this to attention of @alphamoonbase and @mickeyvanolst

cheers
Markus

This thread already was on my radar this morning and I am interrested in also creating a TTT-Game as a little challange.

Regarding MVC: I am certainly an MVC-Stan. All components I released in the last year/s do follow these schema, but also in projects I try to follow this paradigm as it increases maintenability my a huge margin!

1 Like

Thanks! There was a typo in the tox. I meant to say “Remember to avoid doing any kind of state logic inside CHOP Execute DATs and haphazard scripts.”

I’m thinking that the next step for the MVC example is to add a second Python extension that is meant to handling transitions between states, unrelated to visibility of the entire container. This is where I’m headed thinking with ChatGPT but I think I’m out of time programming for today…

Here is my interpretation. Missin the “How to Play” Part but otherwise it does work. You can also select your player icon :slight_smile:

It hinges heavily on two components I built: BanaMash as a finite statemachine with transitions and the sceneCompositor for the UI.
I am not 100% happy with the current state as I have the feeling command structure could be better bundled in the GameController.

There are some inconsistent behaviours in how I do handle data and where they are.
Some Parameters of the datastore do refferences inside the sbcomponents or to attriutes, while other do get set from inside of subcomponents via scripts.I am not happy with this disconnect and need to think about a better/clear approach.

Should the datastore be a target of modification from the subcomponents or should it only reflect the state of the subcomponents?

@alphamoonbase Your TicTacToe is great and probably warrants an entire workshop. Adding an idle mode is such a valid use case.

I’m adding a new version of the MVC thing.
multistate_container.tox (111.2 KB)

I split the concept of state into two dimensions. The first dimension is related to the visibility of the container. This “ContainerState” dimension takes one of six values:
0: Will Appear
1: Appearing
2: Visible
3: Will Disappear
4: Disappearing
5: Invisible

The ContainerState value at any moment is inferred from the timer_container CHOP. The Python extension for the Controller does not hold this value in storage.

The second dimension of state refers to the scene. When subclassing the Python extension, it is up to the user to define the scenes and the allowed transitions between the scenes.

In this specific example, we have scenes A, B, and C. When the container appears we’re always in A. From A we can go to B or C. B and C can go to each other but not A. However, when the container disappear, we can instantly cut to A so that the next time the container appears, we’re in A.

When moving between Scenes, the “TransitionState” is either HOLD, WILL_TRANSITION, or TRANSITIONING.

Thanks for starting this conversation, David … I’ve been banging this drum for some time when working with teams on my own and others’ projects and I’d love to see more standardization around extension usage, and, with it, state machine setups.

There can be a dangerous ambiguity in order of operations between node flows and python scripting execution in complex TouchDesigner applications, and while any given program might be comprehensible if kept in a fixed state, during development that flow is shifting constantly.

For legibility, comprehensibility, debugging, and maintenance, I think it’s critical to bubble up all flow actions to a central business logic extension, and often that extension would benefit from the use of a finite state machine.

Programs that use this style are infinitely more legible and maintainable (and diffable!) than ones where logic is, as you mention, scattered around the app in executes and random script DATs. I’m famously node-allergic and I generally spend 90% of my TouchDesigner dev time in an external IDE scripting a small handful of nodes.

To that end, I avoid delegating anything state-related to the Timer CHOP, and I avoid any designs where state is not stored in, or directly controlled by, the main control extension. While I’ve considered having an Execute DAT call a ‘Tick()’ on that extension onFrameStart() and just doing my own timers with ‘timeElapsed’ kind of stuff, I’m not that dogmatic, but I do keep it to a single Timer CHOP that is strictly used as a dummy timer, and it just calls TimerDone() on the main extension when it’s done, which means all state and logic still lives in the main extension.

The only exception to this is when I have operations that require waiting a very short amount of time before proceeding, eg finishing a write operation before trying to open a file – in those cases I reluctantly use run(delayFrames) to fire a continuation of a process, but I would gladly drop that practice if I could, as I really don’t like the invisible state and brittle flow control implied by that.

Lastly, and again this is personal, I try to use dependable properties sparingly in connecting things to the state of the control extension. It’s convenient but it creates implicit side effects to what in code reads as updating a class member. If I want to update the UI, I give the UI an extension and I call some method whose name explicitly relates to updating the UI. This is important for legibility, both for myself and for other developers diving into the application - the UI isn’t ‘automagically’ kept in sync with the state of the app, it’s updated explicitly at certain times.

@keithlostracco goes even further - he doesn’t use filters or speed CHOPs or anything to smooth value transitions - he tweens them in a Tick() and sets their destination par every time. That way you don’t have that situation where the a value in the extension and the value in the node network don’t agree during the transition, and that way you don’t have to clean up transitions-in-progress that are happening in the node network (in a filter or a speed) when state changes. I’m a little too lazy for this but I’m trying to get into the habit.

My 10 cents. I’m going to do a long-ish video on this soon where I build a basic app I’ve made 1000x - the venerable photobooth - as an example. Will follow up when that’s done.

1 Like

Interresting as this is one of the main things I try to teach people when working with UI for processes: UI has no bussiness being part of the core logic of a project. UI always is only a reflection of the state of a project and never part of it.
This has several reasons: Maintenability and readbility of a project. Not having to polute the business-logic of a project with uneeded calls to the UI is a big one for me. Second is that changes and updates to the UI do not create side-effects with the business-logic. I can work on the UI, try stuff out without having to worry to interfer with the business-logic.

This can either be done via dependables or maybe via an EventEmitter Setup where the UI is listening to events from the controller.

For this reason I am also a big advocate to keep extension themself stateless and always derive the current state of a system based on node-data! I did this myself quiote often, trying to create a shadow-repository of data in python, which always creates issues.

I think logic and controll of data should still be handled by single entities/modules/controller in a project, but the flow should indeed be handled by a single entity at the core of the project as a single source of truth. This entity should not be taking care of handling or modifying any data though, only should be at the helm, sheparding the other modules to do their job.

1 Like

UI has no bussiness being part of the core logic of a project. UI always is only a reflection of the state of a project and never part of it. Second is that changes and updates to the UI do not create side-effects with the business-logic. I can work on the UI, try stuff out without having to worry to interfer with the business-logic.

No arguments on any of this, but the idea of being explicit about the state of the UI and when it’s updated is where we might disagree. I find that the larger and more complex a project is, the more challenging it becomes to debug, and hidden/implicit things, even if they’re not implicated, have to be ruled out as the cause of bugs, which can be painful when they’re basically invisible in the code.

This entity should not be taking care of handling or modifying any data though, only should be at the helm, sheparding the other modules to do their job.

Great callout - this is a good practice for sure. I often have some flavor of DATA top-level extension/op that’s responsible for data and is the single source of truth for that.

This can either be done via dependables or maybe via an EventEmitter Setup where the UI is listening to events from the controller.

For this reason I am also a big advocate to keep extension themself stateless and always derive the current state of a system based on node-data! I did this myself quiote often, trying to create a shadow-repository of data in python, which always creates issues.

Can you say more about this? I feel like what you’re saying is you have the nodes’ pars etc be the authoritative data storage and always pull values from those? That does get you a single source of truth but it makes for some funky, verbose python code and would require writing a bunch of setters/getters to bring type safety up to what you get with keeping your stuff in python. What’s the advantage?

1 Like

More or less, yeah.
The idea is that I get some benefits:

  • I get reactivity out of the gate as almost all of the datastructures are dependable.
  • I can read and understand the state during runtime without either having to do verbose logging or attach a debugger.

One example from my LazyVideoPlaylist, which has N VideoPlayer components and loads new videos etc.

Instead of keeping a dictionary or list of which player is active, which one is waiting etc I simply infer the list based on information given to me by TD itself.

	@property
	def controller(self):
		return sorted(
			self.ownerComp.findChildren(
				name = "videoPlayer[0-99]"),
			key = lambda operator: operator.fetch("loadTimestamp", 0)
		)
	
	@property
	def availableController(self):
		return [ controller for controller 
		  in self.controller 
		 	if controller.Idle
		]
	
	@property
	def readyController(self):
		return [ controller for controller 
		  in self.controller 
		  if controller.Ready and not controller.Running
		]
	
	@property
	def runningController(self):
		return [ controller for controller 
		  in self.controller 
		  if controller.Running
		]

This prevents creating a desync between TD and the structure inside of the extension as the state of the extension always reflects the state of TD itself making it much more errorproof then trying to create a second layer of state on top of the state that is already there.,

Sitenote:
The cool thing is that, in theory, all of these properties are cimpletly reactive/dependable and can be used in a UI without having to create a link at all.

I feel like this is fighting against what TD does provide with the concept of dependables and the idea of the cookChain. I know it can be a pain in the ass from time to time but why try to fight a system that is already in place and lends itself so much to what is there.
But maybe we think about different elements/updates of the UI?
I am mainly talking about stuff like currently selected/active elements or stuff like this, which I for example describe in this article: https://derivative.ca/community-post/article-not-about-top-switcher/69957

Regarding the change of scenes and similar, so the overall-state of a UI, yes, this should not be infered from pure reactivity, but this is what I use the stateMachine for, which is emitting Events on stateChanges, transitions etc that I can use in for example the sceneCompositor.

1 Like

This prevents creating a desync between TD and the structure inside of the extension as the state of the extension always reflects the state of TD itself making it much more errorproof then trying to create a second layer of state on top of the state that is already there.

I can see this argument and I have no quibbles with your example since it directly relates to checking the state of operators in the network. That sort of thing happens a lot is and necessary. The broader goal of having extensions be stateless is where I think I don’t agree. State can involve all sorts of things that are unrelated to the ops in the network - in those cases would you create ops that aren’t strictly necessary just to avoid having state in the extension?

I feel like this is fighting against what TD does provide with the concept of dependables and the idea of the cookChain. I know it can be a pain in the ass from time to time but why try to fight a system that is already in place and lends itself so much to what is there.

It depends how much trust you put in these features and in your ability to reason about them in large-scale applications. I am willing to put in a lot of extra work to make my applications easier to reason about and debug / maintain, including sacrificing brevity and, in many cases, performance.

I think strictly relying on dependables and the cook chain, even if you’re extremely disciplined and skilled at working with them in a purely node-based project, is challenging, but once you mix in scripting via extensions, triggers from outside events, callbacks, etc., is asking for a hairball, often one that only shows up when you’re in heavy dev, ie at the worst time. Granted, I think you’re saying you don’t have logic firing at the other side of dependables, but even with UI updates or what have you, if I can leave a paper trail and be extra double explicit, I’m going to do it.

Thats opens up the question what is strictly necessary :wink:
But yes, I will sometimes do that, having OPs that strictly are there for the purpose of representing data and state, correct. That also kind of is the idea of the MVC-Setup:
The controller being stateless in itself and only managing the model/data/state while the viewer only represents the state. (In some way even a ViewModel might be a good idea only there to represent the Model in a better way for the viewer to handle, which I now often have as part of the controller-logic) but that is another discussion.

Seems our experiences differ in that quite a lot. Would really be interrested in seeing your approach in action.
But also noting that I am not taking about strictly using the cookChain, I also have an EmitterListener setup for events that are triggered and passed arround, but the key should be that there is a one way dependency between systems, especialy with UI.

This has also comes with increased robustness as the UI is simply not able to interfeer with the controller-logic/state as it only is used to delegate calls to the controller while the ui only reflects the state.

lowkey pinging @r-ssek for some insight :wink:

Thanks for the continued discussion. I just shared a project that uses the MVC design I was iterating on. It uses the private methods of extensions in a nice way so that only 3 methods are public:
AskAppear(self) -> bool
AskDisappear(self) -> bool
Transition(self, to_scene: str) -> bool

I’ve wired it up so that hand gestures are detected and call Transition(“A”), Transition(“B”), Transition(“C”). But whether or not the transition actually takes place can be implemented with allow_transition(self, from_scene: str, to_scene: str)-> which defaults to checking whether the current transition state is in a HOLD (i.e., not an active transition). All logic takes place in MyControllerExt and I didn’t use a single CHOP Execute DAT, Execute DAT, etc. There are no timers other than the two needed for the visibility and scene/transition management. Also, the timer callbacks do not need to be modified per project because you just implement methods in MyControllerExt.

video: David Braun on Instagram: "New #touchdesigner file: https://github.com/DBraun/TouchDesigner_Shared/tree/master/Starters/MediaPipeProjects This is inspired by @the.poet.engineer I implemented a state machine similar to sceneChanger that I hope will be useful in everyone’s projects! Props also to @blankensmithing and @domscott.art for MediaPipe in TouchDesigner!"

Just wanted to save people the time of opening the file. I’ll paste the state_ext module at local/modules/state_ext. Skim most of the methods, but I want to highlight the usage of dataclass and how the self.State property gets replaced all at once. For example, when a transition is done:

self.State = replace(self.State, prev_scene=self.State.scene, scene=self.State.target_scene, state=TransitionState.HOLD)

This is so much better than

self.PrevScene = self.CurrentScene
self.CurrentScene = self.TargetScene
self.state = TransitionState.HOLD

or something really hacky like

self.PrevScene, selfCurrentScene, self.state = self.CurrentScene, self.TargetScene, TransitionState.HOLD

So here’s the whole module:

from dataclasses import dataclass, replace
import enum
from typing import Dict, List

from TDStoreTools import StorageManager
import TDFunctions as TDF


# This class is not stored anywhere. It is implicitly stored by the timer_container CHOP.
class ContainerState(enum.IntEnum):
    WILL_APPEAR = 0
    APPEARING = 1
    VISIBLE = 2
    WILL_DISAPPEAR = 3
    DISAPPEARING = 4
    INVISIBLE = 5


class TransitionState(enum.IntEnum):
    HOLD = 0
    WILL_TRANSITION = 1
    TRANSITIONING = 2


@dataclass
class State:
    """
    Encapsulates the entire state.
    """
    scene: str
    state: TransitionState = None  # State in the transition (HOLD, WILL_TRANSITION, TRANSITIONING)
    prev_scene: str = None
    target_scene: str = None
    
    def __post_init__(self):
        if self.prev_scene is None:
            self.prev_scene = self.scene
        if self.target_scene is None:
            self.target_scene = self.scene
    
    def __repr__(self):
        if self.state == TransitionState.HOLD:
            state = "HOLD"
            return f"{self.scene}_{state}"
        elif self.state == TransitionState.WILL_TRANSITION:
            state = "WILL_TRANSITION"
        else:
            state = "TRANSITIONING"
        return f"{self.scene}_{state}_TO_{self.target_scene}"


class ControllerExt:
    """
    ControllerExt.
    """
    def __init__(self, ownerComp, initial_scene: str, allowed_transitions: Dict[str, List[str]]):
        # The component to which this extension is attached
        self.ownerComp = ownerComp
        self.timer_container = ownerComp.op('timer_container')
        self.timer_scene = ownerComp.op('timer_scene')
          
        self.allowed_transitions = allowed_transitions
        
        # Stored items: persistent data like the game board
        state = State(scene=initial_scene, state=TransitionState.HOLD)
        storedItems = [
            {"name": "State", "default": state, "readOnly": False, "property": True, "dependable": True},
        ]

        restoreAllDefaults = True  # set this to True if you're editing the class and need a hard reset.
        self.stored = StorageManager(self, ownerComp, storedItems,restoreAllDefaults=restoreAllDefaults)

    def ask_appear(self) -> bool:
        # User logic to decide whether to actually appear.
        # Subclasses can implement this.
        return True
        
    def AskAppear(self) -> bool:
        # Subclasses shouldn't need to modify this.
        if self.timer_container.segment == ContainerState.INVISIBLE and self.ask_appear():
            self.timer_container.par.start.pulse()  # implicitly go to segment 0
            return True
        return False
            
    def ask_disappear(self) -> bool:
        # User logic to decide whether to actually disappear.
        # Subclasses can implement this.
        return True
        
    def AskDisappear(self) -> bool:
        # Subclasses shouldn't need to modify this
        if self.timer_container.segment == ContainerState.VISIBLE and self.ask_disappear():
            self.timer_container.goTo(segment=ContainerState.WILL_DISAPPEAR)
            return True
        return False
        
    def will_appear_start(self):
        pass
        
    def will_appear_done(self):
        pass
        
    def appearing_start(self):
        pass
        
    def appearing_done(self):
        pass
        
    def visible_start(self):
        pass
        
    def will_disappear_start(self):
        pass
        
    def will_disappear_done(self):
        pass
        
    def disappearing_start(self):
        pass
        
    def disappearing_done(self):
        pass
        
    def invisible_start(self):
        pass
        
    # Methods below are for scenes, not ContainerState.
    
    def allow_transition(self, from_scene: str, to_scene: str):
        """
        A subclass may choose to implement this differently.
        """
        return self.State.state == TransitionState.HOLD
         
    def Transition(self, to_scene: str, instant=False, force=False) -> bool:
        """
        Ask to initiate a transition between two scenes.
        Subclasses shouldn't need to modify this.
        
        Args:
            instant: bool. If True, skip the WILL_TRANSITION and TRANSITIONING. Go straight to HOLD.
        """
        from_scene = self.State.scene
        if force or (self.allow_transition(from_scene, to_scene) and to_scene in self.allowed_transitions.get(from_scene, [])):
            if instant:
                self.State = replace(self.State, scene=to_scene, state=TransitionState.HOLD)
                self.timer_scene.goTo(segment=TransitionState.HOLD)  
            else:
                self.State = replace(self.State, state=TransitionState.WILL_TRANSITION, target_scene=to_scene)
                self.timer_scene.goTo(segment=TransitionState.WILL_TRANSITION)
            return True
        else:
            return False
            
    def hold_start(self):
        # User logic can go here.
        pass

    def _hold_start(self):
        """
        Called when entering HOLD state.
        Subclasses should implement hold_start instead of modifying this.
        """
        self.State = replace(self.State, state=TransitionState.HOLD)
        self.hold_start()
        
    def will_transition_start(self):
    	# User logic for preparation can go here.
    	pass

    def _will_transition_start(self):
        """
        Called when entering WILL_TRANSITION state.
        Subclasses should implement will_transition_start instead of modifying this.
        """
        self.will_transition_start()
        
    def will_transition_done(self):
        # User logic can go here.
        pass

    def _will_transition_done(self):
        """
        Called when exiting WILL_TRANSITION state.
        Subclasses should implement will_transition_done instead of modifying this.
        """
        self.State = replace(self.State, state=TransitionState.TRANSITIONING, target_scene=self.State.target_scene)
        self.timer_scene.goTo(segment=TransitionState.TRANSITIONING)
        self.will_transition_done()
        
    def transitioning_start(self):
        # Logic for transition effects can go here.
        pass

    def _transitioning_start(self):
        """
        Called when entering TRANSITIONING state.
        Subclasses should implement transitioning_start instead of modifying this.
        """
        self.transitioning_start()
        
    def transitioning_done(self):
    	pass

    def _transitioning_done(self):
        """
        Called when exiting TRANSITIONING state.
        """
        self.State = replace(self.State, prev_scene=self.State.scene, scene=self.State.target_scene, state=TransitionState.HOLD)
        self.timer_scene.goTo(segment=TransitionState.HOLD)
        self.transitioning_done()
        
    def while_transition_active(self, fraction: float):
    	pass

    def _while_transition_active(self, fraction):
        """
        Called continuously during the transition process.
        """
        self.while_transition_active(fraction)
        
    def while_hold(self):
        pass
    
    def _while_hold(self):
        """
        Called when in HOLD state.
        Subclasses should implement while_hold instead of modifying this.
        """
        self.while_hold()

And the project specific subclass in a separate DAT:

from state_ext import ControllerExt, TransitionState
		

class MyControllerExt(ControllerExt):
	def __init__(self, ownerComp):
		
		initial_scene = "A"
		allowed_transitions = {
            "A": ["B", "C"],
            "B": ["A", "C"],
            "C": ["A", "B"]
        }
		
		super().__init__(ownerComp, initial_scene, allowed_transitions)
		
	def button_hide_clicked(self):
		self.AskDisappear()

	def disappearing_done(self):
		pass
				
	def invisible_start(self):
		# Ensure we're in state A when we appear next.
		if self.State.scene != "A":
			self.Transition("A", instant=True, force=True)
		op('scenes/gesture_state_A').par.Reset.pulse()
			
	def hold_start(self):
		print(f"Holding in scene: {self.State.scene}")
		
	def will_transition_start(self):
		print(f"Preparing to transition from {self.State.scene} to {self.State.target_scene}.")
		
	def transitioning_start(self):
		print(f"Transitioning from {self.State.scene} to {self.State.target_scene}.")
		op(f"scenes/gesture_state_{self.State.target_scene}").par.Reset.pulse()
	
	def transitioning_done(self):
		print(f"Transitioned from {self.State.prev_scene} to {self.State.scene}.")
		
	def while_transition_active(self, fraction: float):
		print(f"Transition progress: {fraction * 100:.2f}%")
		
	def while_hold(self):
		pass
		# Logic to automatically bounce between B and C every 5 seconds.
		#if self.State.scene in ["B", "C"] and self.State.state == TransitionState.HOLD:
			#if self.timer_scene['cycles'] > project.cookRate*5:
				#self.Transition("B" if self.State.scene == "C" else "C")

Then there’s a sceneChanger-esque base that blends between the current scene’s data and the target scene’s data. In this base, two select CHOPs use Python expressions:
f"../scenes/out1_points_{parent.Controller.State.scene}"
and
f"../scenes/out1_points_{parent.Controller.State.target_scene}"
A Blend CHOP uses the timer_scene CHOP’s fraction to blend between those two selects.

I like the lack of external dependencies in this approach, David, but if you ever feel like you’ve a need for more batteries-included state machine stuff, I’d recommend Transitions (pytransitions)

I’ve been using this for almost all my state-related projects and I find I end up using tons of the features it offers as I progress into making more complex experiences, especially games. Worth checking out.

Yeah now I want to see your EmitterListener setup!

In this repo you will find two implementations/interpretations.
The explicit Emitter/Listener setup is the one I use the most. The manager is more a proof of concept but feels much harder to read.

The idea is simple. You have en EmitterComp in which you define a set of Events using json.
You can then have N Listeners which Point to the specified emitter.
Calling the Emit function on the Emitter will invoke the callback on all listeners subsribed to the Emitter.
The Emitter has a strictMode, checking that events emitted adhere to the jsonDefiniton as does it offer a networkBridge so this can also be used to emit multi-process events.

The key is that the Emitter also offers the AttachEmitter method, allowing any Extension/COMP to behave like an emitter.
This is done in my InitCOMP as does my finite stateachine do this.


This workflow allows me to for example have a central statmachine, router or whatever to controll the project-flow and handle the specified events like stateEnter in the subcomponents.

_
In adapted some workflows from doing quite some webdev during covid using Vue and NuxtJS (this also kinda is where the whole statelesness, datastore idea comes from) and also try to not fight TD and its nodebased environment but to embrace it.
I could of course do all of that purely in python and not use any nodes, but I personaly like the readability of this approach (Emitter is a Comp, Listener is a Comp etc.)

Some sleepless nights I kept thinking about this and I cam up with the following component that should implement several things you are talking about @hardworkparty and @DavidBraun
As I said, many of the things I take inspiration from is WebDevelopment (I would not call me by any means a webdev but I had my share of experience by now) and the frameworks like Vue certainly have some stuff figured out.

The more I though about this I came to the realisation that the thing you are working on david is pretty close to a router. (Or a router also is just a statemachine…)
So I tried to implement something similiar for TD: BananaRouter (daring, I know…)

The idea is relatively simple: You define a given set of Routes in callbacks by returning a List of Route-Inherting classes, which need to implement the name and path attribute.
As a bonus you can also overwrite the preEnter and the preExit methods.
During this “Guard”-Method you can either call self.Abort($reason) to stop the transition, or self.Suspend(“Reason”). to halt the transition.
If the transition is only halted using suspend, it can be resumed using .Resume on the Router itself. (tbi) [ Here we could use a timer for transitions, or the Tweener etc. for some “TransitionState” ]

A definition looks like this:

def defineRoutes(router: "extBananaRouter", RouteClass : "Route") -> list["Route"]:
	class Home(RouteClass):
		Name = "Home"
		Path = "/"

		def preExit(self, target:"Route", router:"extBananaRouter"):
			if math.random() > 0.5: self.Abort("Its pure chance")

	class Detail(RouteClass):
		Name = "Detail"
		Path = "/detail/:id"

		def preEnter(self, source:"Route", router:"extBananaRouter"):
			if self.Params.id.lower() in ["foo", "bar"]: 
				self.Abort(f"Used on of the evil words: {self.Params.id}")
			

	
	return [Home, Detail]

We can now call Push to navigate to a given route.
First the preExit guard will be called, then the preEnter one. If both run without exceptions the rensition will happen and an Event will be Emitter from the Router which can be catched using the eventListenerCOMP to trigger wanted events.
The Route objects also cover dynamic matching, indicated by the : in the URI which can be acessed using Route.Params.

The Router itself does show the Active Route Object via .ActiveRoute, allowing to recheck current active params during runtime.

EDIT:
@DavidBraun Did you dabble with TDAsyncIO by any chance?
I did a quick test with a timerCHOP and this works quite well.


from asyncio import sleep as asyncSleep

async def startTimer(timer:timerCHOP, length:float):
    debug("Starting")
    timer.par.length.val = length
    timer.par.initialize.pulse()
    timer.par.start.pulse()
    while not timer["done"].eval():
        await asyncSleep(0)
    debug("Awaited")

op("TDAsyncIO").Run( startTimer(op("timer1"), 2) )

Next up is a datastore with Mutators :slight_smile:

So I took a little moment to dig deeper in to asyncIO and reworked the complete Router to work completly async and tbh this feels so fresh!

Here I rebuilt the OlibBrowser (or better something replacing it in the future) to be completly controlled by the router.
Still missing proper errorhandling but otherwise it works.
i only have to figure out how stuff like “PlaceOPs” is working.

Let me know what you think of this paradigm.

import asyncio

def defineRoutes(router: "extBananaRouter", RouteClass : "Route") -> list["Route"]:

	class Search(RouteClass):
		Name = "Search"
		Path = "/search"

		async def onEnter(self, source:"Route", router:"extBananaRouter"):
			op("SceneCompositor").Take("Search", .1)
			pass

	class Detail(RouteClass):
		Name = "Detail"
		Path = "/project/:slug"

		async def onEnter(self, source:"Route", router:"extBananaRouter"):
			request:"Request" = iop.Target.Detail( self.Params.slug )
			op("SceneCompositor").Take("Working", .1)
			while request.status in ["Idle", "Running"]:
				await asyncio.sleep(0)
			op("SceneCompositor").Take("Detail", .1)
			pass

	class Download(RouteClass):
		Name = "Download"
		Path = "/download/:release_id"

		async def onEnter(self, source:"Route", router:"extBananaRouter"):
			download:"Download" = iop.Target.Download( self.Params.release_id )
			op("SceneCompositor").Take("Working", .1)
	
			while download.status in ["Idle", "Running"	]:
				# Upadting the UI Element.
				op("SceneCompositor_Items/Download").par.Size.val 		= download.size
				op("SceneCompositor_Items/Download").par.Downloaded.val = download.size * download.progress
				op("SceneCompositor_Items/Download").par.Estimated.val 	= download.estimatedTime
				await asyncio.sleep(0)
			
			# Replacing / with \ so the router will not think that the downloadpath is part of the route.
			backslashedFilepath = str(download.filepath).replace('/', '\\')
			router.Push(f"/place/{backslashedFilepath}")
			pass
	
	class Place(RouteClass):
		Name = "Place"
		Path = "/place/:reversedFilepath"

		async def onEnter(self, source, router):
			op("SceneCompositor").Take("Place", .1)
			# This is not working. There is some weirdness with placeOPs and I think it fucks with async.
			while iop.Target.Place( self.Params.reversedFilepath.replace('\\', '/') ) != "Placed":
				await asyncio.sleep(0)
			router.Push("/search")

	return [Search, Detail, Download, Place]