More Trouble with Lists and TurnScripts

As a fledgling little coder who really only understands it when the syntax makes sense to him, I admit that it may be what I'm trying to do can be gone about in a better way.

Here's what I'm trying to do, and I thought a TurnScript might be the way to go: Every turn I want to scan the world (ie. all locations reachable by the player, which I have been exclusive adding to a directory labeled Locations, conveniently enough) for objects with the alias "doe" and "buck." I have created two deer objects outside the game world to pull from. I hope to clone from these two deer, labeled deerDoe and deerBuck, and respawn a random one when they die.

As far as I can tell, my code should do the following: create a master list of the two deer to pull from, called MasterDeer. Then, it should create the list for AllDeer, which should be checking all the child objects in the Locations directory, adding them to the list if they're alive, and removing them when they're dead. If the list contains less than 2 deer, I want to generate another one randomly, pulling from the two deer on the MasterDeer list. I've tried several versions of syntax to try to get this to work as intended, but since each iteration I try throws errors, so I must be missing something fundamental about the way empty lists work. What if I want to find out if a list is empty, and if so, fill it?

MasterDeer = NewObjectList ()
  list add (MasterDeer, deerBuck)
  list add (MasterDeer, deerDoe)
AllDeer = NewObjectList ()
foreach (object, GetAllChildObjects (Locations)) {
  if (object.alias = "doe") {
    if (object.dead = false) {
      list add (AllDeer, object)
    }
    else {
      list remove (AllDeer, object)
    }
  }
  else if (object.alias = "buck") {
    if (object.dead = false) {
      list add (AllDeer, object)
    }
    else {
      list remove (AllDeer, object)
    }
  }
}
if (ListCount (AllDeer) < 2) {
  NewDeer = PickOneObject (MasterDeer)
  CloneObjectAndMove (NewDeer, HG11)
  foreach (object, NewDeer) {
    if (object.alias = "doe") {
      list remove (NewDeer, object)
    }
    else if (object.alias = "buck") {
      list remove (NewDeer, object)
    }
  }
}
else {
  DisableTurnScript (DeerTurn)
}

Or more likely, I'm misunderstanding something about the way I'm trying to call these objects into my lists. Here's my current error.

Error evaluating expression 'ListCount(l) = 0' ListCount function expected list parameter was passed null.


Hmm… this seems a pretty complex function.

The first issue I see is your management of the AllDeer list. It goes through each object inside of Locations, and finds the deer.
If they're not dead, it adds them to AllDeer.
If they are dead, it removes them from AllDeer … which I expect will cause an error, because you're removing something from a list that you haven't added it to.

After making this list, the only thing you seem to do with it is count how many objects are on the list. It might be sensible to make a list if you're doing something else with them in future. But if you're only counting them, it's probably easier just to use a number to keep track of how many you found.

Then, if the number of the deer is less than two, it gets a bit confusing.
You pick one of the deer prototypes and store it in a variable NewDeer. You then attempt to use foreach to loop over NewDeer, which fails because it isn't a list. And even if it were a list, the code would fail; because you're not allowed to add/remove from a list during a foreach over the same list.

At the end, if there are 2 or more deer in Locations, it disables the turnscript… so I guess once you have two deer, you never want to spawn more? Or will something else enable the turnscript again later?

Also: you use the aliases "doe" and "buck" to identify the object. This is generally a bad habit to get into: if you're looking for clones of two particular items, that's what the prototype attribute is for: it points to the original that was cloned. And for weird technical reasons, comparing two objects is faster than comparing two strings.

So… taking this into account, I think you could use something like:

MasterDeer = NewObjectList ()
list add (MasterDeer, deerBuck)
list add (MasterDeer, deerDoe)

deerCount = 0
foreach (object, GetAllChildObjects (Locations)) {
  if (ListContains (MasterDeer, object.prototype)) {
    if (not GetBoolean (object, "dead")) {
      deerCount = deerCount + 1
    }
  }
}

if (deerCount < 2) {
  NewDeer = PickOneObject (MasterDeer)
  CloneObjectAndMove (NewDeer, HG11)
}
else {
  DisableTurnScript (DeerTurn)
}

(in this version, rather than comparing object.prototype to both "buck" and "doe", and doing the same thing for each, I checked whether object.prototype (the object that an object was cloned from) is in MasterDeer; which I think should have the same effect. It just means that if you later add another type of deer, you can just add it to the list instead of needing to modify the code.)

If you were to put the code in a function, there would be a more efficient way to do it:

MasterDeer = NewObjectList ()
list add (MasterDeer, deerBuck)
list add (MasterDeer, deerDoe)

deerCount = 0
foreach (object, GetAllChildObjects (Locations)) {
  if (ListContains (MasterDeer, object.prototype)) {
    if (not GetBoolean (object, "dead")) {
      deerCount = deerCount + 1
      if (deerCount >= 2) {
        DisableTurnScript (DeerTurn)
        return ()
      }
    }
  }
}

NewDeer = PickOneObject (MasterDeer)
CloneObjectAndMove (NewDeer, HG11)

(return in this case being used just to stop the running function; if you already found 2 deer, there's no reason to keep on checking the remaining objects)


To me, this sounds like the hard way to do it...
Why not create a "map" that the deer use.
One that has what rooms connect to what rooms.
when a deer is killed, spawn a new deer in a random room, and have it move to any connecting room
either before or after the player moves.
That way, either the player will move into a room where the player is, OR, the deer will move into the room the player is.
This sounds like a hunting simulation.
Or... if you don't need moving deer, then do it by random number before the player enters the room.
No need to track where they are.


@DarkLizerd

To me, this sounds like the hard way to do it...
Why not create a "map" that the deer use.
One that has what rooms connect to what rooms.
when a deer is killed, spawn a new deer in a random room, and have it move to any connecting room
either before or after the player moves.

That sounds even more complex to me; and doesn't seem to address the same problem.

The initial code seems to be intended to create a deer at a fixed location every turn until there are two, and then stop. The given turnscript does seem a little overengineered for the purpose; but I assume this is just the first test for a technique which will later be used with larger numbers of clonable objects.

If they just wanted a new deer to be spawned in response to one being killed, then I would agree that rather than counting the number of live deer, it would make more sense to just call SetTurnTimeout from the deer's death script, causing another one to be spawned the following turn or after a delay. Unless there's a chance that several will be killed at the same time, and you want to ensure that they are recreated at a rate of one per turn.


Hmm… that's got me thinking now. If a system like this is being used for multiple types of creatures, I think it would be a lot more efficient to have a single turnscript for all of them. So how would I go about organising something like that?

I think I'd create a function SetupRespawningCreature with 4 parameters: groupName, minimumCount, objectToClone, and spawnLocation. The script would be something like:

if (not HasAttribute (game, "respawnGroups")) {
  game.respawnGroups = NewDictionary ()
}
if (not DictionaryContains (game.respawnGroups, groupName)) {
  dictionary add (game.respawnGroups, groupName, QuickParams ("name", groupName, "target", minimumCount, "objects", NewObjectList()))
}
dict = DictionaryItem (game.respawnGroups, groupName)
objList = DictionaryItem (dict, "objects")
if (not DictionaryContains (objList, objectToClone)) {
  list add (objList, objectToClone)
}
if (minimumCount > 0 and not Equal (DictionaryItem (dict, "target"), minimumCount)) {
  dictionary remove (dict, "target")
  dictionary add (dict, "target", minimumCount)
}
if (not DictionaryContains (dict, "locationTable")) {
  loclist = NewObjectList ()
  dictionary add (dict, "locationTable", QuickParams (objectToClone.name, loclist))
}
else {
  table = DictionaryItem (dict, "locationTable")
  if (DictionaryContains (table, objectToClone.name)) {
    loclist = DictionaryItem (table, objectToClone.name)
  }
  else {
    loclist = NewObjectList ()
    dictionary add (table, objectToClone.name, loclist)
  }
}
list add (loclist, spawnLocation)
EnableTurnScript (CreatureRespawn)

And a turnscript called CreatureRespawn (initially disabled, so it doesn't run until there are creatures for it to watch for):

liveCount = NewDictionary()
foreach (object, FilterByAttribute (GetAllChildObjects (Locations), "dead", false)) {
  name = object.prototype.name
  if (DictionaryContains (liveCount, name)) {
    count = DictionaryItem (liveCount, name)
    dictionary remove (liveCount, name)
    dictionary add (liveCount, name, count + 1)
  }
  else {
    dictionary add (liveCount, name, 1)
  }
}
foreach (entry, game.respawnGroups) {
  needed = DictionaryItem (entry, "target")
  objects = DictionaryItem (entry, "objects")
  foreach (obj, objects) {
    if (DictionaryContains (liveCount, obj.name)) {
      found = DictionaryItem (liveCount, obj.name)
      needed = needed - found
    }
  }
  if (needed < 0) {
    objectToSpawn = PickOneObject (objects)
    table = DictionaryItem (entry, "locationTable")
    locations = DictionaryItem (table, objectToSpawn.name)
    loc = PickOneObject (locations)
    CloneObjectAndMove (objectToSpawn, loc)
  }
}

Written off the top of my head; and a lot more complex than it should be because of the ugliness of Quest's dictionaries. I kind of got carried away there, to be honest. But I think with this script, you could put in your start script:

SetupRespawningCreature ("deer", 2, deerBuck, HG11)
SetupRespawningCreature ("deer", 2, deerDoe, HG11)

to create a respawn group called "deer" which respawns a deerBuck or a deerDoe at location HG11 each turn until there are two of them. If you add the same creature to a respawn group with multiple locations, it can appear in multiple places; and you can have the same creature in multiple respawn groups. This is a lot more complex to initially set up, but it means that you could make it work with other creatures just by adding new SetupRespawningCreature lines, and the same code will handle all of them.

(I didn't actually test the code; wrote it straight on the forum… a habit I really need to get out of when I see people talking about an interesting system)


I couldn't get this out of my head; and realised that it could be easier to use. So, a little modification of the setup function:

if (not HasAttribute (game, "respawnGroups")) {
  game.respawnGroups = NewDictionary ()
}
objectToClone = GetObjectList (objectToClone)
spawnLocation = GetObjectList (spawnLocation)
if (not DictionaryContains (game.respawnGroups, groupName)) {
  dictionary add (game.respawnGroups, groupName, QuickParams ("name", groupName, "target", minimumCount, "objects", NewObjectList()))
}
dict = DictionaryItem (game.respawnGroups, groupName)
objList = DictionaryItem (dict, "objects")
dictionary remove (dict, "objects")
dictionary add (dict, "objects", ListCompact (ListCombine (objList, objectToClone)))
if (minimumCount > 0 and not Equal (DictionaryItem (dict, "target"), minimumCount)) {
  dictionary remove (dict, "target")
  dictionary add (dict, "target", minimumCount)
}
if (not DictionaryContains (dict, "locationTable")) {
  table = NewDictionary ()
  dictionary add (dict, "locationTable", table)
}
else {
  table = DictionaryItem (dict, "locationTable")
}
foreach (obj, objectToClone) {
  if (DictionaryContains (table, obj.name)) {
    loclist = DictionaryItem (table, obj.name)
    dictionary remove (table, obj.name)
  }
  else {
    loclist = NewObjectList ()
  }
  dictionary add (table, obj.name, ListCompact (ListCombine (loclist, spawnLocation)))
}
EnableTurnScript (CreatureRespawn)

This depends on a second function, GetObjectList with a single parameter input and type "objectlist":
(edited: the first two lines were the wrong way around when I originally posted this)

output = NewObjectList ()
switch (TypeOf (input)) {
  case ("object") {
    if (not input = null) {
      list add (output, input)
    }
  }
  case ("objectlist") {
    return (input)
  }
  case ("list", "stringlist") {
    foreach (item, input) {
      output = ListCompact (ListCombine (output, GetObjectList (item)))
    }
  }
  case ("dictionary", "stringdictionary", "scriptdictionary", "objectdictionary") {
    foreach (key, input) {
      item = DictionaryItem (input, key)
      output = ListCompact (ListCombine (output, ListCombine (GetObjectList (key), GetObjectList (item))))
    }
  }
  case ("string") {
    foreach (element, Split (input)) {
      obj = GetObject (element)
      if (not obj = null) {
        list add (output, obj)
      }
    }
  }
}
return (output)

This means that if you have a bunch of creatures and any of them can appear in several locations, you could do something like:

ApesList = NewObjectList ()
list add (ApesList, gibbon)
list add (ApesList, gorilla)
list add (ApesList, orangutan)
SetupRespawningCreature ("ape", 3, ApesList, "farmyard;jungle;nest;lab;cage;UFO")
SetupRespawningCreature ("ape", 3, spaceMonkey, UFO)

So you have three apes that can spawn in a list of locations, and one which only spawns at a single location. All of these have the same chance of being created if there are less than 3 apes on the map.
Both the creature type and location can now be an object, a string containing the object name, a stringlist, an objectlist, or a string containing object names separated by semicolons.


This is actually quite incredible, and I'd love to give this a whirl. All I can say is that this is extremely robust, and that I have several sections of the game I'm building that will benefit hugely from this system. I will say that upon first entering the code, the GetObjectList function did not compile, though I haven't been through it to see where there might be a missed bracket potentially. Some of the syntax in that code is a little beyond my knowledge on how to format, so it's possible I wouldn't catch it no matter how long I looked. Hahaha... But as soon as I have these sections a little more fleshed out, I will attempt to implement this system because it sounds awesome and flexible.


Sorry... I had the first two lines of that function the wrong way around. Editing the post now. Not sure how I managed that.


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

Support

Forums