How to create infinite maze?

Basically I make a 5x4 rooms.
Each of the exits from one to another room is referred as walls, so they have 50% chance of appearing.
This gives us a random maze of a 5x4 rooms size.

Each 5x4 rooms size have a X-coordinate and Y-coordinate named as game.x and game.y.
So there is an enter door at the bottom of the 5x4 dungeon, and an exit door at the top of the 5x4 dungeon.
When the players moves to the exit door, game.y = game.y+1, then move the player to the enter door at the
bottom of the 5x4 dungeon.
So far, everything works great and the game knows that the dungeon are different based on the xy coordinates.

But this is where I encounter a problem, how do I make the game remember that the object chair and walls exist
in this specific room of this specific dungeon of xy coordinates? I could make that the object chair is visible when game.x=0
and game.y=1, but the chair are randomly generated, meaning I clone chair and move to player.parent which is the player's current location, but I cannot access to change the chair's attributes, meaning I cannot make the chair knows that it is in x=0, y=1, if the chair knows, it will be visible, otherwise, invisible.

Another failed solution is that whenever a player enters a room, create a Floor and a Chair, whenever there is a Floor in the
room, the generator will not create anything, this solves my problem of auto-generating, but when tested in real life, the game
does not recognize the Floors, because when you clone an object, it will create Floor1 instead of Floor, because the game cannot handle objects with exactly same names.


OK, there's a lot to unpack here. I'm not sure I understand your post completely, but I'll include as much detail as I think is needed. Sorry if you already have some of this, or it seems irrelevant.

There are two common ways to make an infinite maze, and you seem to have selected a kind of hybrid.

The most common way to do this is to have an inaccessible 'template' room somewhere. Whenever the player goes through an exit that doesn't lead anywhere, the template room is cloned and added to the map at that location. This means you don't need to keep track of what is in each room, because Quest does that for you.
For more variety, you could even have a variety of rooms to choose from, and clone a different one each time.

Another method for a random dungeon is to have a single room, and then modify its attributes and move objects in/out of it to represent the different things that might be found there based on the player's coordinates. So rather than moving the player to a new room, you have an exit which changes the appearance of the current room and then displays the new description. Kind of like in the theatre, changing the backdrop and props to represent travelling.
This is often an efficient method for modelling a large dungeon; but Quest doesn't make it easy. Remembering what was in each room so you can display it again next time can become quite complex. It's doable, but Quest's data structures aren't well-suited to the task.

If I understand you correctly, you are using this second method but with a block of 20 actual rooms. I can see that makes sense, but Quest might make it a little harder to work with compared to other engines.

I clone chair and move to player.parent which is the player's current location, but I cannot access to change the chair's attributes

In this case, you should be able to access the chair's attributes just fine. I would expect you to have something like:

thischair = CloneObjectAndMoveHere (Chair)

which creates an object (possibly named Chair1 or Chair4 or similar), and stores it in the variable thischair. You can then use thischair as if it were the object for the remainder of the script. For example, you could do:

thischair.x = game.x
thischair.y = game.y

to give the newly created chair a couple of attributes indicating which dungeon it is in.

You could then set it up so that when the player moves to a different x/y, all the objects in their current location are then removed, and if the player is returning to coordinates they've visited before, the objects from those coordinates are restored.
There are three main ways you could go about this:

  1. Hide the objects that are in the wrong x/y coords.

You would loop over all the objects in the rooms, showing or hiding them depending if their x/y match the current location. For this example I'm assuming there is a room DungeonContainer which contains the 20 rooms; as this makes it easier to loop over them:

foreach (room, GetDirectChildren (DungeonContainer)) {
  foreach (object, GetDirectChildren (room)) {
    if (HasInt (object, "x") and HasInt (object, "y") and not Contains (game.pov, object)) {
      object.visible = (object.x = game.x and object.y = game.y)
    }
  }
}

That will show all objects whose x and y match the current game.x and game.y and hide objects whose x and y are different. In this case it won't do anything with objects that don't have coordinates.

If you want it to work properly if the player picks up an object and drops it elsewhere, you would also need to make sure that objects they are carrying have the correct coordinates. Then the objects will behave correctly when dropped. After changing game.x and game.y but before showing and hiding objects, you would do something like:

foreach (object, ScopeInventory()) {
  object.x = game.x
  object.y = game.y
}
  1. Remove objects when the player changes coordinates; find them and bring them back when the player returns to those coordinates.

This method is a little more complex, as you also need to store which room the objects were in. However, it might be a better option if you already have invisible objects, so that the visible flag isn't serving two purposes.

In this case, you'd first want to remove objects from the current rooms, and store their location for future access:

foreach (room, GetDirectChildren (DungeonContainer)) {
  foreach (object, GetDirectChildren (room)) {
    if (not (object = game.pov or Contains (game.pov, object))) {
      object.x = game.x
      object.y = game.y
      object.room = room
      RemoveObject (object)
    }
  }
}

note that this doesn't delete the objects; it just removes them by moving them into non-player space. In this case I'm specifically testing for the player and their objects, so that they don't get whisked away.

Once we've removed those objects, you increase game.y, and then check if there are any objects waiting to be moved back. In this case we end up looping over all objects to find the ones we want. we might be able to make the code easier to read using FilterByAttribute, but we would be creating multiple temporary objectlists, which is slower and uses more memory.

foreach (object, AllObjects()) {
  if (object.parent = null) {
    if (Equal (object.x, game.x) and Equal (object.y, game.y)) {
      object.parent = object.room
    }
  }
}

and once all that is done, you can move the player to their new room, so the description will be displayed using the recently moved objects.

  1. Actually destroying unnecessary objects

In theory you could destroy any objects that are floating around; store the random numbers used to generate each room in some set of attributes, and use those numbers to clone the objects again when they're needed. But this is already a complex thing to do, and if your objects have any attributes it will become even harder.

whenever there is a Floor in the room, the generator will not create anything, this solves my problem of auto-generating, but when tested in real life, the game does not recognize the Floors, because when you clone an object, it will create Floor1 instead of Floor

Rather than testing if the object named Floor is here, you want to test if there is a clone of Floor here. The clone functions (CloneObject, CloneObjectAndMove, CloneObjectAndMoveHere, and CloneObjectAndInitialise) will all give an object a prototype attribute which can be used to find its clones.

For example, the expression FilterByAttribute (AllObjects(), "prototype", Floor) will give you an objectlist of all clones of Floor.

In this case, a more useful expression would beFilterByAttribute (GetDirectChildren (game.pov.parent), "prototype", Floor) which examines all objects in the current room to see if any of them are clones of Floor.

You could use this like:

floorlist = FilterByAttribute (GetDirectChildren (game.pov.parent), "prototype", Floor)
if (ListCount (floorlist) = 0) {
  // do whatever you wanted to do if there *isn't* a floor here
}
else {
  thisfloor = PickOneObject (floorlist)
  // if you want to do anything when there is a floor here, you can do it here
  // you can use the variable `floorlist` to access the floor object in this room
  // or just remove the else clause if you don't need to do anything
}

I think that should be everything you need in order to make this work.
Is there anything I've missed?


mrangel have the correct solution for infinite maze, if his solution helps you, you can help him by checking out his books.
http://textadventures.co.uk/forum/general/topic/qlnz6tgk2u6rufhzykrpxw/has-mrangel-ever-helped-you

You didn't missed anything, you even added more solutions for problems I have not predicted yet.
I like to make easy games or apps to slowly learn about quest textadventure and coding, but it looks like I have stumbled upon a beast.
It would take me some time to code the above then I can reply you whether I am able to do it.

Thanks a lot!

Update:

Your first common way is to clone room, I cannot find such an option in quest, it might be done in code view programming, and I googled out https://textadventures.co.uk/forum/quest/topic/muyzncmskuamclqqeiqw9w/cloning-a-room-contents which is too much for me to do, and I do not even know how to begin with programming "Whenever the player goes through an exit that doesn't lead anywhere", so I shall skip this method.

You second common method is to use single room, this is probably the easiest method for me to deal with, you pointed out a flaw with this method but this flaw does not applies to me, I am basically making rpg games, like randomly generating monsters with stats that are at a lower ratio to player's stats and then sceneries that does not do anything, but I get what you mean, if I decide to expand the game, It will be hard to memorize the map even if I draw a map in real life, because every room will have multiple objects for me to record down.

Your formula below shockingly works and I do not know why :) (Do not bother to re-explain me, I am slow.)

thischair = CloneObjectAndMoveHere (Chair)
thischair.x = game.x
thischair.y = game.y
foreach (room, GetDirectChildren (DungeonContainer)) {
  foreach (object, GetDirectChildren (room)) {
    if (HasInt (object, "x") and HasInt (object, "y") and not Contains (game.pov, object)) {
      object.visible = (object.x = game.x and object.y = game.y)}}}
  1. To explain why I made a 5x4 dungeon instead of a 1x1 dungeon, it is because I am making an infinite maze, within a 5x4 dungeon, I can predetermined open paths from the north, south, east and west doors to each other, usually in a + crossroads.
    For a 1x1 dungeon, each north, south, east and west have 20% of becoming closed paths, it is unlikely to have fully closed paths and players gets stuck, but if the players do encounter such heavy unluckiness, the maze is not infinite anymore.

  2. But there is a better way to create an infinite maze with 1x1 dungeon, basically the start of xy coordinates = 1 to 5, rooms of 25 have open paths from the very start of maze, after that, further paths will have 20% of becoming closed paths. I do not know the maths, but perhaps there is 400% chance of open path on NSEW, the next path will have 320%, the next path will have 256%, actually we will probably have to write out such a dungeon to see whether it works.

  3. The last way to create an infinite maze with 1x1 dungeon is to spawn 3x3 closed rooms, however this type of map does not gives me the feeling of a narrow hedge maze, which is the feeling I am trying to replicate.


The "single room and put stuff in there" is how the original text adventures did it. (And the way I did it in Basic before finding Quest. Still find the idea of objects strange. And clones even stranger!)
The nice thing about programming is that 10 people can give to 20 ways to solve a problem, and in the end, you come up with number 21, that no one else even thought about.


Apparently the forum do not like me editing my old post for new updates.

Me rephrasing MrAngel's solution to try to understand it:

thischair is confusing because it sounds like this(object) or chair(object), but it is just random word for a variable, meaning I can put x1 to replace thischair instead, which I tested and it still works. If thischair is an object, it would not works, as it can only gains 1 set of x and y, however it is actually a variable, a variable is just a temporary value which disappears after the script has ended. MrAngel used a variable to link up CloneObjectAndMoveHere (Chair).xy = game.xy, because if you directly write my formula, the game do not accepts such equation. So another way to simplify this is: ``` x1 = CloneObjectAndMoveHere (Chair) x1.x = game.x x1.y = game.y ``` Now for the next program: foreach ``` foreach (room, GetDirectChildren (DungeonContainer)) { foreach (object, GetDirectChildren (room)) { if (HasInt (object, "x") and HasInt (object, "y") and not Contains (game.pov, object)) { object.visible = (object.x = game.x and object.y = game.y)}}} ``` foreach means to run this same script multiple times. (room,) means to keep the result of the script to this variable. (, GetDirectChildren(DungeonContainer)) means to get all the room folders inside the parent folder (DungeonContainer).

The next line of code

foreach (object, GetDirectChildren (room))

is similar, run same script multiple times, to get all childrens of room folder which gives us all the objects, and the result is kept in the variable called object.

Third line of code

if (HasInt (object, "x") and HasInt (object, "y") and not Contains (game.pov, object))

We retrieve the variable object from previous 2 lines of codes, and test whether they have attributes x or y, and whether they do not have point of view (game.pov), after much googling, game.pov seems to refer to all objects inside player's folder which is basically the player's inventory.
not Contains (game.pov, object) means excluding all the objects in the player's inventory, meaning the player's inventory is excluded from the above if code. I am thinking this not Contains (game.pov, object) is not important, because generally player's inventory are usually and already visible. The whole 4 lines of code is to make objects visible, but the code is excluding player's inventory when making objects visible, but like I said, player's inventory are usually visible.

Lastly

object.visible = (object.x = game.x and object.y = game.y)}}}

object.visible means to make all the objects in this variable "object" visible, the right hand side means make it visible only if the variable object have an x coordinate = to the current game x coordinate.

The code can be rewritten as below for easier understanding.

foreach (x2, GetDirectChildren (DungeonContainer)) {
foreach (x3, GetDirectChildren (x2)) {
if (HasInt (x3, "x") and HasInt (x3, "y") and not Contains (game.pov, x3)) {
x3.visible = (x3.x = game.x and x3.y = game.y)}}}

Our next code is fairly simple

foreach (object, ScopeInventory()) {
  object.x = game.x
  object.y = game.y}

ScopeInventory code returns an objectlist containing all the visible objects which the player has in their inventory.
So foreach code again, for each object in player's inventory, change the object xy coordinates to the new xy coordinates.
This code is used when player entering a map of different xy coordinates, so that when the player decides to drop an inventory, it will have the correct xy coordinates.


For MrAngel's solution 2, it is recommended to start on a new quest file, it would be useful to keep the old file for future referencing.

foreach (room, GetDirectChildren (DungeonContainer)) {
  foreach (object, GetDirectChildren (room)) {
    if (not (object = game.pov or Contains (game.pov, object))) {
      object.x = game.x
      object.y = game.y
      object.room = room
      RemoveObject (object) }}}

So we input this code, we find out that our doors(object) disappeared, whenever our autogenerator creates new object like a Chair(object), the Chairs disappears too. So if you look at the code similar to MrAngel's first solution, it digs out all objects in the game except for the player and the player's inventory.
object = game.pov refers to the player.
Contains (game.pov, object) refers to the player's inventory.
RemoveObject (object) like MrAngel said, it moves the object away instead of destroying, I am guessing this code moves the object out of parents/rooms.
object.room = room refers to adding new attributes to all objects, the attribute is named room, this code is strange as it records the current room name, this is probably useless in 1x1 dungeon infinite maze, this might be for 4x5 dungeon infinite maze, but, in short, its utility is strange.

I am skipping FilterByAttribute

This next code do not seems to works.

foreach (object, AllObjects()) {
  if (object.parent = null) {
    if (Equal (object.x, game.x) and Equal (object.y, game.y)) {
      object.parent = object.room}}}

It basically takes out all objects and store them in variable object,
Find all the objects have no parents/room, then amongst them,
find objects with same xy coordinates as the current map xy coordinates,
Finally, change this object's parent into a room.

Why it does not works is because the doors appears but the autogenerated old Chairs reappear whenever the player
enters a new map of different xy.
While we do not have an answer for that, after rereading, we know that solution 2 is better than solution 1 because it does not
relies on visibility, but rather on rooms instead, therefore we could still introduce invisible objects in our game.
Because that is the main difference, our coding error lies in that area too, we are missing some lines of coding about rooms.


For MrAngel's solution 3, start a new quest file and keep the old file for future referencing.

floorlist = FilterByAttribute (GetDirectChildren (game.pov.parent), "prototype", Floor)
if (ListCount (floorlist) = 0) {
  // do whatever you wanted to do if there *isn't* a floor here
}
else {
  thisfloor = PickOneObject (floorlist)
  // if you want to do anything when there is a floor here, you can do it here
  // you can use the variable `floorlist` to access the floor object in this room
  // or just remove the else clause if you don't need to do anything}

We know that this code is about destroying, we will try to learn more tomorrow.
For this code, it is called FilterByAttribute, FilterByAttribute returns a new object list containing only the objects in the given list for which the named attribute has the given value.

floorlist = FilterByAttribute (GetDirectChildren (game.pov.parent), "prototype", Floor)

floorlist is most likely a variable, FilterByAttribute gives us something,
(GetDirectChildren (game.pov.parent) are the parents of the players which is the room player is at right now.
"prototype" is an attribute, Floor is used here so we can find out about it.
In short, variable = Find(player's room, "prototype" attribute, value=Floor)
So to rephrase it again, Find prototype Floor in the player's room and gives us that value inside a variable, so like MrAngel said, the prototype means that this Floor is a clone, therefore this line of code is extremely important to all our future games and coding, because I am going to streamline my coding to autogeneration as autogeneration codes uses one set of code to play 100 to unlimited rooms, as compared to making 100 codes for 100 rooms, which I have tried in my previous games, I will give up and surrender!

This infinite maze lesson is too hard and deep for me to understand, I will use whatever works based on any combination of MrAngel's solution rather than understand it, perhaps few years later, I will be back to try and understand it again.

Creating clones baffle me too...
So, why use them?
The player is only in 1 room at a time.
why not place the "one and only" chair in the current room (if one is needed), when the player enters the room?
You could even randomize the chair's description each time. But, I see where the chair would keep changing colors if the player jumps back and forth between 2 rooms with the chair in it. In that case, add an attribute to the chair based on the room number (.X and .Y not needed.)
4x5 rooms numbered this way:
01, 02, 03, 04, 05
06, 07, 08, 09, 10
11, 12, 13, 15, 15
16, 17, 18, 19, 20
exit=room 03
start room=18
I should be very easy to randomize the room contents like this:
// random floors
(pseudo code because I think in BASIC and must translate to Quest)
for a=0 t0 20
x=rnd(6) ' 6 different floors
floor(a)=x ' make room#A.floor= random floor# X
next a '
Now you have the room floor descriptions set.
Floor="5,2,4,1,3,2,5,5,2,3,4,2,1,3,6,6,1,3,2,3"
And do the same for the chair
0=no chair, >0 = a chair of the random color
Now, when the player enters the room, just read the floor(room#) to get the floor,
and chair(room#) to place the chair, and the same for every other item you want to add.
If the player picks up the chair, change chair(room#)=0 (no chair)
Altho, this would limit each room to only having 1 chair.
This isn't object programming, but it does work the same way, altho, if the chair IS an object, then would it count?


For DarkLizerd, creating clones might be required as I generally make rpg games, I have to create multiple enemies like AxeDwarf1 and AxeDwarf2, players have the option to retreat after battling halfway, meaning there is one dwarf with half hp, and the other with full hp. (If it matters, I am using autogeneration monsters, objects, everything, so cloning seems relevant even if I am no master of it.)
I am going to take some time to read the rest of your code.

Guys stop giving me additional code for Infinite Maze, LOL, I only know if else programming, anymore is stressing me out for now.
I cannot even comprehend MrAngel's solution and I do not know BASIC code or any other programming language.


Give me a few days and I can slap an example together. Right now, I'm getting my Dark Halloween re-working, and expanded.


OK, I looked over what you said at the start.
What you are creating is a maze with no exit, it just goes on forever.
And you are using a 5x4 grid to "describe" part of it.
What happens when you "exit" the top of the maze and jump to the bottom, and start running over your old rooms?
Why not use a 10x10, and pre-generate the maze?
And have a start and end point?


MrAngel, I need help, I used the following variation of your codes but the program only clones the Floor object once only,
I am guessing line 6 FilterByAttribute have a mistake in which it detects Floor object in other rooms, that is why it will not clone any new Floor object, I need help to correct this.

foreach (object, GetDirectChildren (room)) {
  if (HasInt (object, "x") and HasInt (object, "y") and not Contains (game.pov, object)) {
    object.visible = (object.x = game.x and object.y = game.y)
  }
}
floorlist = FilterByAttribute (GetDirectChildren (game.pov.parent), "prototype", Floor)
if (ListCount (floorlist) = 0) {
  // do whatever you wanted to do if there *isn't* a floor here
  thisfloor = CloneObjectAndMoveHere (Floor)
  thisfloor.x = game.x
  thisfloor.y = game.y
}

Additional non-important notes : I am going for 1 room infinite maze, the player moves according to x and y.
To handle a maze-like structure, there is a 50% of spawning north, south, east, west roads (objects.), for example, if north road object is visible, the exit to North is visible too, else not. To make the maze makes sense, when creating a north road at x:1 y:1, I will have to spawn an additional south road and Floor at x:1 y:2.

After trying three more times, once with game.x%2=0, exit visible,
once with game.x>3 and game.x<9 and game.y<0 and game.y<5, exit visible,
lastly with pre put create object Floorcode4, find current player x and y in game, the next game.x-1 to game.x+1 have exits closed from game.y of -1 to +6.
I think I should quit making infinite maze, bye bye XD!
Faints in my bed.


Update 27 April 2021, MrAngel's new code

floorlist = FilterByAttribute (GetDirectChildren (game.pov.parent), "prototype", Floor)
floorlist = FilterByAttribute (floorlist, "visible", true)

Infinite maze finally seems to be working, still working hard on it to make a simple maze.


@DarkLizerd
Well, this infinite maze is becoming sorta a nightmare.
Yes I need to create a maze with no exit, after making so many games, I realized creating rooms is troublesome, a chore, a mess, time-exhausting, hard to find the last room I was working on.
I previously decided to use 5x4 grid because I can pre-do many walls and make it feel like the hedge maze just like in all the movies, also with 5x4 grid I can pre-do an open path to ensure there are actually exits, now this plan is currently abandoned.

In order to avoid "exit" the top of maze and jump to bottom and start running over old rooms, I used the game attribute x and y,
for example the top of maze have x:0 y:1, the original maze is x:0 y:0, this way, you can determine which objects to hide if their coordinates does not match, according to MrAngel's code it should be object.visible = (object.x = game.x and object.y = game.y)

If I pre-generate the maze, have a start and end point, I am not improving myself, everytime I make a game, I must learn and create something new, and I feel that this infinite maze is a huge stepping stone to become a super good game creator.


MrAngel, I need help, I used the following variation of your codes but the program only clones the Floor object once only,
I am guessing line 6 FilterByAttribute have a mistake in which it detects Floor object in other rooms, that is why it will not clone any new Floor object, I need help to correct this.

foreach (object, GetDirectChildren (room)) {
  if (HasInt (object, "x") and HasInt (object, "y") and not Contains (game.pov, object)) {
    object.visible = (object.x = game.x and object.y = game.y)
  }
}
floorlist = FilterByAttribute (GetDirectChildren (game.pov.parent), "prototype", Floor)
if (ListCount (floorlist) = 0) {
  // do whatever you wanted to do if there *isn't* a floor here
  thisfloor = CloneObjectAndMoveHere (Floor)
  thisfloor.x = game.x
  thisfloor.y = game.y
}

Ah, I think I was a little careless with that one. In that case, it's checking whether there is a floor in that room, rather than checking if there's a visible floor in that room.

There's two methods to ignore floors in other versions of this room. The most efficient would be to filter the floor list to only the ones that are visible:

floorlist = FilterByAttribute (GetDirectChildren (game.pov.parent), "prototype", Floor)
floorlist = FilterByAttribute (floorlist, "visible", true)

Or slower (but the code might be easier to understand):

floorlist = FilterByAttribute (ScopeVisibleNotHeld(), "prototype", Floor)

Infinite Maze can now be played at
https://textadventures.co.uk/games/view/wyxv0ib9jkopqyza1nhsww/infinite-maze-2021

Free code at bottom.
Copy and paste into Quest via [Tools, Code View] or [F9].

I think I cannot post big amount of code in forum as I tried but it wasn't visible,
the code is now viewable at https://pastebin.com/9MLaacUX


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums