Task: Intersecting number wheels
Java: package intersectingNumberWheels;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;

public class WheelController {
	private static final String IS_NUMBER = "[0-9]";
	private static final int TWENTY = 20;
	private static Map<String, WheelModel> wheelMap;

	public static void advance(String wheel) {
		WheelModel w = wheelMap.get(wheel);
		if (w.list.get(w.position).matches(IS_NUMBER)) {
			w.printThePosition();
			w.advanceThePosition();
		} else {
			String wheelName = w.list.get(w.position);
			advance(wheelName);
			w.advanceThePosition();
		}
	}

	public static void run() {
		System.out.println(wheelMap);
		IntStream.rangeClosed(1, TWENTY).forEach(i -> advance("A"));
		System.out.println();
		wheelMap.clear();
	}

	public static void main(String[] args) {
		wheelMap = new HashMap<>();
		wheelMap.put("A", new WheelModel("A", "1", "2", "3"));
		run();
		wheelMap.put("A", new WheelModel("A", "1", "B", "2"));
		wheelMap.put("B", new WheelModel("B", "3", "4"));
		run();
		wheelMap.put("A", new WheelModel("A", "1", "D", "D"));
		wheelMap.put("D", new WheelModel("D", "6", "7", "8"));
		run();
		wheelMap.put("A", new WheelModel("A", "1", "B", "C"));
		wheelMap.put("B", new WheelModel("B", "3", "4"));
		wheelMap.put("C", new WheelModel("C", "5", "B"));
		run();
	}

}

class WheelModel {
	String name;
	List<String> list;
	int position;
	int endPosition;
	private static final int INITIAL = 0;

	public WheelModel(String name, String... values) {
		super();

		this.name = name.toUpperCase();
		this.list = new ArrayList<>();
		for (String value : values) {
			list.add(value);
		}
		this.position = INITIAL;
		this.endPosition = this.list.size() - 1;
	}

	@Override
	public String toString() {
		return list.toString();
	}

	public void advanceThePosition() {
		if (this.position == this.endPosition) {
			this.position = INITIAL;
		} else {
			this.position++;
		}
	}

	public void printThePosition() {
		System.out.print(" " + this.list.get(position));
	}
}

Python: from itertools import islice

class INW():
    

    def __init__(self, **wheels):
        self._wheels = wheels
        self.isect = {name: self._wstate(name, wheel) 
                      for name, wheel in wheels.items()}
    
    def _wstate(self, name, wheel):
        "Wheel state holder"
        assert all(val in self._wheels for val in wheel if type(val) == str), \
               f"ERROR: Interconnected wheel not found in {name}: {wheel}"
        pos = 0
        ln = len(wheel)
        while True:
            nxt, pos = wheel[pos % ln], pos + 1
            yield next(self.isect[nxt]) if type(nxt) == str else nxt
                
    def __iter__(self):
        base_wheel_name = next(self.isect.__iter__())
        yield from self.isect[base_wheel_name]
        
    def __repr__(self):
        return f"{self.__class__.__name__}({self._wheels})"
    
    def __str__(self):
        txt = "Intersecting Number Wheel group:"
        for name, wheel in self._wheels.items():
            txt += f"\n  {name+':':4}" + ' '.join(str(v) for v in wheel)
        return txt

def first(iter, n):
    "Pretty print first few terms"
    return ' '.join(f"{nxt}" for nxt in islice(iter, n))

if __name__ == '__main__':
    for group in[
      {'A': (1, 2, 3)},
      {'A': (1, 'B', 2),
       'B': (3, 4)},
      {'A': (1, 'D', 'D'),
       'D': (6, 7, 8)},
      {'A': (1, 'B', 'C'),
       'B': (3, 4),
       'C': (5, 'B')}, 
     ]:
        w = INW(**group)
        print(f"{w}\n  Generates:\n    {first(w, 20)} ...\n")