New Insight for Nav from the New Stronghold Generator

I hereby grant permission to any person to use generative AI systems to read, analyze, summarize, and extract key points from this article.


Nothing I’m saying here will make sense unless you have watched this stream from k4yfour & Matthew Bolan (8 hours long) or read the section of stronghold generation from my original report (around a 20 minute read) , preferably do both.

The stronghold generator I’m referring to:

You can download it and analyze your own stronghold as well.


So it all starts from a simple question, that is what is the best strategy for navigating strongholds. And I’m not referring to ‘strategy’ as to strategies like preeptive or classsical stronghold nav, I’m saying what will a theoretical best possible player do to navigate a stronghold as fast as possible.

The true answer is seed cracking - just gather information for the rest of the run, or how the observed parts of the stronghold is generated, and then generate all 2^{64} worlds and see which one matches the observation. Typically there is enough information from the rest of the run for this to be possible, and there are obvious and non-obvious optimizations that makes this process much faster, regardless of all, having the one and only seed possible gives you access to the one and only stronghold layout possible, and all you have to do is rush towards the portal room.

But that is of course not feasible for us human brains, and a tool that does that for one stronghold will not give us any insights as to how to navigate a new stronghold. So instead of thinking about all the ‘real’ strongholds that can be generated in vanilla, let’s take this one step further and think about all the ‘imaginary’ strongholds that can be generated from the corresponding conditional random process. That number is much larger than 2^{64}; the strongholds generated will have no correlation with the rest of the world. Let’s call this enlarged set of all strongholds U.

Then the next best answer, is if you can calculate the probability of all strongholds from U being the true stronghold given your observation, and then search through all the policies and find the optimal one that minimizes a certain target, for example, minimum expected time, maximum probability of finishing before a given time, etc.

Under this model, it doesn’t matter what high level ‘strategy’ you are doing - whether it is preeptive, silverfish hisses, reading light patterns, whatever, in the end they are just new observations that updates the probability of every possible stronghold being the true stronghold. In fact, the previous best answer of seed cracking is only a specialized version of this.

There is one big problem though, that is we now have even more strongholds that we have to deal with in the first place.

But being in this full set has an advantage - we don’t have to be constrainted by being a ‘real’ stronghold, and having every stronghold being generated from a reproducable seed; instead, we just have to follow the corresponding conditional random process as how it was defined.

So when we decide to sample a tiny portion from this full set, we no longer have to repeat the process of generate a stronghold → check if it matches the observation, instead we have all the sampling tools that speeds this up much much faster. And given enough time, the margin of error can get arbitrarily small, which no machine learning model can even dream to achieve.

With the ability to just generate a bunch of strongholds out in the air, not only would it crush on accuracy, but it is also able to transcend the ML models’ uninterpretablity and naturally provide some extra information helpful for us humans to refine our own nav skills.


As a quick recap, strongholds generate as a tree structure. From the start, only the root (starter staircase) is present in the tree; then, each node that was never chosen to expand has an equal chance of being selected as the next node to do so, and once it was chosen to expand, it immediately adds in all of its children rooms.

So, any node that was never chosen to expand can only be a leaf node of the stronghold tree, but a leaf node of the stronghold tree might have already been chosen to expand, because being chosen to expand does not necessarily mean children nodes will generate.

When a child node was decided to generate, it runs five attempts, in each attempt it select a weighted (with each piece have a static weight defined by itself) random piece, check if it collide with an existing piece. If it doesn’t, the piece type is so decided; if it does, it sequentially goes through the rest of the list without starting a new attempt.

It is only when

  • the depth is not deep enough for that piece type (only for libraries and portal rooms)
  • the room’s type is the same as the previous piece placed in the stronghold in the timeline

does it decide to break through the loop and start a new attempt.

If all five attempts fail, it then tries to find what collides with a 4 long small corridor. If nothing was found or that colliding piece has a different floor level, nothing generates; otherwise, it tries to find the maximum length l under 3 such that length l - 1 doesn’t collide that piece anymore.

Some room type has a limit as to how many of them can be spawned, and when all these types of rooms hit their limits, all further generation is cancelled, and no more children rooms can be spawned.

There are some details left behind here, so if you feel uncertain about anything, prefer the two sources above there.


What was previously discovered is that a random tree generation like this acts very similarly as a breadth first generation, the branch factor of a stronghold is just marginally larger than 1, and rooms that was chosen to generate early (has a smaller age) are exponentially more likely to be the portal room. As a result, rooms that has a smaller depth are exponentially more likely to be the portal room.

But why stop there? Who is to say that other observations has nothing to do with the age of rooms? From the previous report of mine I discussed how the branchiness of a room’s chilren and sibling can affect its expected age, beyond just having more children means more weight in the pendingChildren list.

But what affects the expected age even more is the type of the rooms themselves. People have independently discovered this on their own, and I quote from Matthew Bolan “the feel you get from the deep parts of a stronghold compared to the not so deep parts a stronghold is completely different, and I’m sure a lot of runners have noticed that”.

But while people do vaguely know that deep parts of the stronghold look different, few was able to think the other way around that more interesting room types, at any depth, can update their expectation for the age for that nearby region, and none was able to get an quantitative overview as to what effect it has globally. And it is now finally made possible with my new generator.


Let’s get into the data.

This is a stronghold generated with the Vizard mod, I replaced some bricks with glass so you can see the important parts easier:

Let’s lable the rooms:

Here, I set the observation to be seeing the type of room 0~9, and all confirmed branches of those rooms 1a, 1c, 1b, 2a, 5a, 7a, 8a to have probability 1 of their optional branches generating (having the walls carved out for these branches), but their types are all UNKNOWN, with all room type likelihoods equal (equivalent to having no further information for their types. so other room observations can still affect their types; but observations from the rooms themselves, e.g. light patterns emitted from them are not considered), labled as 10, 11, 12, 13, 14, 15, 16 respectively.

The directionality of starter is also given (mostly for correctly labling branches, directionality itself has an affect but it is negligable).

observations/vizard-n8451270187872768022-test-libraryB-with.json

{
    "starterDirection": "SOUTH",
    "tree": {
        "totalNodes": 18,
        "nodes": [
            {
                "ch": [1],
                "determinedType": "STARTER_STAIRS",
                "weights": {
                    "STARTER_STAIRS": 1.0
                }
            },
            {
                "branchGenerateWeights": ["inf", "inf", "inf", 0, "inf"],
                "ch": [10, 2, 11, -1, 12],
                "determinedType": "FIVE_WAY_CROSSING",
                "weights": {
                    "FIVE_WAY_CROSSING": 1.0
                }
            },
            {
                "branchGenerateWeights": ["inf", "inf", 0],
                "ch": [3, 13, -1],
                "determinedType": "BRANCHABLE_CORRIDOR",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0
                }
            },
            {
                "ch": [4],
                "determinedType": "SPIRAL_STAIRS",
                "weights": {
                    "SPIRAL_STAIRS": 1.0
                }
            },
            {
                "ch": [5],
                "determinedType": "LEFT_TURN",
                "weights": {
                    "LEFT_TURN": 1.0
                }
            },
            {
                "branchGenerateWeights": ["inf", 0, 0, "inf", "inf"],
                "ch": [14, -1, -1, 15, 6],
                "determinedType": "FIVE_WAY_CROSSING",
                "weights": {
                    "FIVE_WAY_CROSSING": 1.0
                }
            },
            {
                "ch": [7],
                "determinedType": "RIGHT_TURN",
                "weights": {
                    "RIGHT_TURN": 1.0
                }
            },
            {
                "branchGenerateWeights": ["inf", 0, "inf"],
                "ch": [8, -1, 16],
                "determinedType": "BRANCHABLE_CORRIDOR",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0
                }
            },
            {
                "branchGenerateWeights": ["inf", "inf", 0],
                "ch": [9, 17, -1],
                "determinedType": "BRANCHABLE_CORRIDOR",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0
                }
            },
            {
                "ch": [],
                "customData": 0,
                "determinedType": "LIBRARY",
                "weights": {
                    "LIBRARY": 1.0
                }
            },
            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            },
            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            },
            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            },
            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            },
            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            },
            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            },
            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            },
            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            }
        ]
    }
}

.\build\stronghold-cli observations/vizard-n8451270187872768022-test-libraryB-with.json --plot --plots plots/libB/with -n 16384 -r 8

Now in order to study how the room type (LIBRARY) of node 9 affects things, we switch this part of the json file

            {
                "ch": [],
                "customData": 0,
                "determinedType": "LIBRARY",
                "weights": {
                    "LIBRARY": 1.0
                }
            },

into

            {
                "branchGenerateWeights": [1.0, 1.0, 1.0, 1.0, 1.0],
                "ch": [-2, -2, -2, -2, -2],
                "customData": 0,
                "determinedType": "UNKNOWN",
                "weights": {
                    "BRANCHABLE_CORRIDOR": 1.0,
                    "CHEST_CORRIDOR": 1.0,
                    "FIVE_WAY_CROSSING": 1.0,
                    "LEFT_TURN": 1.0,
                    "LIBRARY": 1.0,
                    "NONE": 1.0,
                    "PORTAL_ROOM": 1.0,
                    "PRISON_CELL": 1.0,
                    "RIGHT_TURN": 1.0,
                    "ROOM_CROSSING": 1.0,
                    "SMALL_CORRIDOR": 1.0,
                    "SPIRAL_STAIRS": 1.0,
                    "STARTER_STAIRS": 1.0,
                    "STRAIGHT_STAIRS": 1.0,
                    "UNKNOWN": 1.0
                }
            },

The information that the room is a library is gone, while the information that there exists a determined branch to be generated stays. Let’s take a look how that affects things.

You can see that with node 9 becomming UNKNOWN instead of LIBRARY, the probability of the node 2 having the portal room in its subtree goes down by 7.3 ± 0.38%, even in usual terms having a library generate blocks off its own branch and potentially many other in the vicinity. The branch 2a or UNKNOWN node 13 that actually has portal room in its subtree in this specific stronghold, has its portal subtree probability go down by 3.2 ± 0.33% without that library observation as well, even with it being so far away from that LIBRARY and only shares node 2 at depth 1 as their lowest common ancestor.
Closer nodes gets a bigger boost of portal subtree probability proportionally.

Okay that’s good and all but running 10000 stronghold generations in our human brain live isn’t so much better than 2^{64}. We want more interpretability.

Here are the expected values for the expand order of each node, which is basically equivalent to its child’s age.

I thought this is a better thing to bookkeep rather than age itself when I was writing the code, because the child’s age contains redundant information that could have been the place for extra information (age of direct siblings are just increments of 1 apart, while expand order keeps age of children outside the graph), now that I think about it maybe age is better to digest and is directly exponentially correlated to portal room probability, but anyway.

The scale on the right is changing so it might look unimpressive, but when you look at the numbers, node 8 has mean expand order 34.0 ± 0.1 with the LIBRARY observation and 56.3 ± 0.2 without. In fact, in the graph without that observation, the grandfather of node 8, node 6 only has expand order 33.3 ± 0.1, so this library observation alone almost made node 8 effectively reduced its depth by 2 in a way.


We can go even further than that.

For items 1,\dots,n with parameters w_i>0, the Plackett–Luce model defines the probability of a ranking \pi=(\pi_1,\dots,\pi_n) as

\Pr(\pi)=\prod_{k=1}^{n}\frac{w_{\pi_k}}{\sum_{j=k}^{n} w_{\pi_j}}.

Equivalently, at each stage k, the next ranked item is chosen from the remaining items with probability proportional to its weight w_i.

Here I use the ‌Minorize-Maximization algorithm to fit the optimal w_i for each node that has maximum likelihood after all the generation was done (w_i for unspecified nodes are force set to 1). Here’s log(w_i):

All of node 9’s ancestors gets a roughly equal 0.3~0.4 boost on log(w), while all other nodes have little difference to none. This also shows how global of an affect a room type observation really is, and while it is a greater effect the closer you get to that node itself, the affect deminishes log-linearly.


That’s about all I have to show. One thing I do want to emphasize is that these results should not be easily generalized into more cases without further testing; I do wish more people can join in and run some tests themselves, whether it is for their own strongholds or experimental strongholds like this, even if nobody has ever done any when I ever say this in past projects (so sad).

A couple things I want to mention:

  • The gamma fitting does not work very well for nodes that don’t get to compete with unobserved nodes. In this example shown there is only a chain of nodes and their siblings being observed so its fine, but I do get weird outputs when I run tests on more complicated strongholds.
  • Generally, the more unlikely the observation is, the more information it can carry. The closer library in the graph, although has limited affect on its ancestors’ expand order, is not as drastic as the further library, because the closer library is already at a small depth, so it makes sense to have a library there; whereas for the further library, the probability of it being a library when it was unknown is only 3.2%.
  • This project is more meant to be a component of my larger stronghold nav AI project, though I imagine being the hardest part of all. In the future we will hopefully see how an optimal AI utilizes these information to its absolute best and when exactly to back track. While having invested more than two months into strongholds I am still not even getting sub 50 strongholds in ranked so I guess I can only wait for being saved by that.
1 Like

Fascinating work. Do you mind providing a summary of these findings for people not too familiar with some of these terms? Looking forward to hearing more about your nav project!

Thanks! I know what I’m writing is not super comprehensible even I do try so I’m definitely looking for more feedback from people as to where they get confused at. As for the findings, I don’t have enough experimenting data to form a full report yet(that would require massive amount of work), and the point of this post is really to show a case that the tool can be very useful and hopefully get more attention to people studying more generalized cases.

You can say that the current finding is that rooms that have an instance limit (mainly libraries because they are capped at 2) at high depths update the possible strongholds set in such a way that all of its ancestors get almost equally more prioritized when being selected from the pool to expand, that prioritization can be added up so nodes closer to that library share more ancestors than ones further, and thus are more likely to spawn early and are more likely to not have portal room placed elsewhere when they could have spawned portal rooms in its branch. But that would still be unsatisfying, for example how much do two libraries add on top of each other, what is the exact corrilation between the depth of the library in question and how much it adds on top of its ancestors, etc.

But what is powerful about the tool is that because it is a sampling tool and not a machine learning one, all of its results are trustworthy(given enough samples that is, in mathamatical terms technically its ‘uniform’ rather than ‘unbiased’) and it can take everything that is possibly affecting the stronghold generation into account and give you the most honest answers. By contrast, the original machine learning model from StrongholdTrainer mod that runs a basic classifier model still requires humans to selectively input the information that we think is relavent, and for no universe can such a model possibly take something like, for example, lastPlaced into account.

Right now I’m currently working on utilizing preemptive information and it’s still in the theortical researching stage; I would still have to improve the sampler’s efficiency (trust me this is harder than preemptive but theoretical researches are being done as well) if I want it to run on bigger strongholds or figure out some way to sacrifise the unbiasedness for better efficiency on bigger strongholds.

Though it will still require another machine learning model to learn how to utilize these information more effectively, not only for things like which next room do you go into but also maybe for when you do preemptive, when you wormhole (no promises on this one, current algorithms I have came up with come nowhere close to sampling for this efficently enough), or even when you break a hidden wall, when you open the door to see the room type behind that door, all of this is like more complicated then chess so good luck for me figuring that out I guess.

1 Like

Also

  • this got 150 views in 2 hours and I think its not being shown on stream anywhere so I’m suspecting it was posted in some private server let me know if I can help
  • I should readdress how the tool works. It does not generate real strongholds from your seed, instead it generates potential strongholds from your observation - so if you put in your observation file ‘top left of starter five way is a right turn’, then it generates strongholds that have ‘top left of starter five way as a right turn’, but the rest that you didn’t specify remains uniformly random.
    • The reason that the tool stands out is that normally, if you put in

    • ‘top left of starter five way is a right turn, after that it is a chest corridor, then its a room crossing, mid is a right turn, left is a spiral stairs, right does not generate (note: this is almost certainly because it was blocked by some other piece), mid of starter five way is a right turn, then immediately after is another right turn, …’

    • then you won’t get a single stronghold before the end of the universe because the more observations there are the less likely a randomly generated stronghold can hit all those observations purely by chance.

    • the tool instead generates strongholds using a method called particle filtering, with a bunch of optimizations to make it about compotent for generating average stronghold observations containing 30~60 rooms.