Difference between revisions of "Denizen Herding Behavior"
(How to get your creatures to herd together.) |
m |
||
(24 intermediate revisions by 4 users not shown) | |||
Line 1: | Line 1: | ||
This article is by Russell Ackerman. | This article is about getting your denizens to herd together to a greater or lesser degree and is by Russell Ackerman. | ||
You can download the full code by downloading the game Ascii Wilderness: http://asciiwilderness.blogspot.com/p/ascii-wilderness.html | |||
check out "ghoul.lua" | |||
I thought I would share my success at getting my deer to herd in my roguelike, I did it by doing this: | I thought I would share my success at getting my deer to herd in my roguelike, I did it by doing this: | ||
This article relates to the LUA programming language | This article relates to the LUA programming language. | ||
Sound propogates out from a square, dropping | |||
The deer herd together by making a sound each turn, and then when they move to another square, they value those squares according to how much sound there is (but only checking sound for a fraction of the turns that it moves) and move onto the square with the most sound from it's "friends" | |||
"makesound" currently gets called once per turn per creature, | |||
soundscape gets nilled out each game turn and re-initailized with [x][y][z] values of the map, prepared to take new entries., | |||
and moveenemytowards selects a square based on sound and distance to destination information. | |||
the strength of herding is mostly controlled by monster.herdingtendency. | |||
Sound propogates out from a square, dropping information about itself. The function is as follows: | |||
Intensity is generally an integer. At intensity 20, the sound will travel outwards 20 squares and use a lot more processor than intensity 5, per se. | |||
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="lua"> | |||
function funcs.makesound(intensity,activator,x,y,z) | function funcs.makesound(intensity,activator,x,y,z) | ||
-- | local objectloc = data.objectloc --contains [x][y][z][object with internal data] | ||
local working, future = {}, { {x,y,z} } | |||
local working, future = {}, {{x,y,z}} | local soundscape = data.soundscape --where your sound data is stored, per tile [x][y][z][the object][thesound - arbitrary key][various keys like strength of the sound] | ||
local soundscape = data.soundscape | |||
local finished = {} | local finished = {} | ||
local insert = table.insert | local insert = table.insert | ||
local tiles = data.tiles | local tiles = data.tiles | ||
local checkforedgeofmap = funcs.checkforedgeofmap | local checkforedgeofmap = funcs.checkforedgeofmap --so sound doesnt go off the edge of your limited world. | ||
finished[x] = {} | finished[x] = {} | ||
finished[x][y] = {} | finished[x][y] = {} | ||
Line 20: | Line 32: | ||
local counter = intensity | local counter = intensity | ||
while(counter <= intensity and counter >= 1) do | while(counter <= intensity and counter >= 1) do | ||
counter = counter - 1 | |||
counter = counter - 1 --this keeps track of how strong the sound is when it gets to the square. | |||
working,future = future, {} | working,future = future, {} | ||
local x,y,z | local x,y,z | ||
Line 28: | Line 41: | ||
for y = v[2] -1,v[2] + 1 do | for y = v[2] -1,v[2] + 1 do | ||
finished[x][y] = finished[x][y] or {} | finished[x][y] = finished[x][y] or {} | ||
for z = v[3] - 1,v[3] + 1 do | for z = v[3] - 1,v[3] + 1 do --now we're iterating over the nine squares around the center square, on the first iteration. | ||
if z < data.amountofzlevels and z >= 1 and (not checkforedgeofmap(x,y)) and (not finished[x][y][z]) and (not tiles[x][y][z]["filled"]) and (not tiles[x][y][z]["open"]) then | if z < data.amountofzlevels and z >= 1 and (not checkforedgeofmap(x,y)) and (not finished[x][y][z]) and | ||
(not tiles[x][y][z]["filled"]) and (not tiles[x][y][z]["open"]) then | |||
finished[x][y][z] = 1 | finished[x][y][z] = 1 | ||
insert(future,{x,y,z}) | insert(future,{x,y,z}) | ||
soundscape[x][y][z][activator] = soundscape[x][y][z][activator] or {} --for moveenemytowards | soundscape[x][y][z][activator] = soundscape[x][y][z][activator] or {} --for moveenemytowards | ||
insert(soundscape[x][y][z][activator],{["strength"] = counter}) | insert(soundscape[x][y][z][activator],{["strength"] = counter}) | ||
end | end | ||
end | end | ||
Line 44: | Line 53: | ||
end | end | ||
end | end | ||
end | |||
end</syntaxhighlight></div> | |||
----------------------- | ----------------------- | ||
Data about the "soundscape" is stored, for now, just with the data of who made the sound and how intensly it was heard at the square. soundscape[x][y][z][the object that activated it][irreleveant key][strengthofsound] and any other pertinent variables about the noise would also be stored there. | |||
Data about the "soundscape" is stored, for now, just with the data of who made the sound and how intensly it was heard at the | |||
square. soundscape[x][y][z][the object that activated it][irreleveant key][strengthofsound] and any other pertinent variables about | |||
the noise would also be stored there. | |||
To use this data in pathfinding to get your denizens to herd, simply do the following: | To use this data in pathfinding to get your denizens to herd, simply do the following: | ||
1/x times that the creature activates pathfinding to choose a square with the shortest path, simply weight the RELATIVE VALUE of those squares. Squares with more noise generated by creatures who are the same species as me should be weighted with an amount relative to the total noise on that square from those animals except myself. Its a simple matter of using a function like "total_sound_at_square_except_me(me,x,y,z). | 1/x times that the creature activates pathfinding to choose a square with the shortest path, simply weight the RELATIVE VALUE of | ||
those squares. Squares with more noise generated by creatures who are the same species as me should be weighted with an amount | |||
relative to the total noise on that square from those animals except myself, weighted to be MORE valuable, but only one out of x times - this fractional usage allows the creature to break from the herd and stop herding behavior once it cant hear any sound at all from those of its own species. Its a simple matter of using a function like "total_sound_at_square_except_me(me,x,y,z). | |||
My code for moving the denizens is at follows. the table "Distances" is a table of data in the form distances[x][y][z] = amount, where distances[x][y][z] is the distance of a particular square to the destination square. It's a little hokey but it's pretty simple too: | My code for moving the denizens is at follows. the table "Distances" is a table of data in the form distances[x][y][z] = amount, where distances[x][y][z] is the distance of a particular square to the destination square. It's a little hokey but it's pretty simple too: | ||
All I do is subtract sound from distance, and select the square with the "lowest" relative value to move the denizen to. | |||
<div style="padding: 5px; border: solid 1px #C0C0C0; background-color: #F0F0F0"><syntaxhighlight lang="lua"> | |||
function funcs.totalsoundfromob(ob,x,y,z) | function funcs.totalsoundfromob(ob,x,y,z) | ||
local soundscape = data.soundscape | local soundscape = data.soundscape | ||
local totalstr = 0 | local totalstr = 0 | ||
for k2,v2 in pairs(soundscape[x][y][z][ob]) do | for k2,v2 in pairs(soundscape[x][y][z][ob]) do | ||
totalstr = totalstr + v2["strength"] | totalstr = totalstr + v2["strength"] | ||
end | end | ||
return totalstr | return totalstr | ||
end | end | ||
function funcs.totalsoundatspot(ob,x,y,z) --totals sounds at spot from "my species" | function funcs.totalsoundatspot(ob,x,y,z) --totals sounds at spot from "my species" | ||
local soundscape = data.soundscape | local soundscape = data.soundscape | ||
local sound | local sound | ||
local totalstrength = 0 | local totalstrength = 0 | ||
local numfriends = 0 | local numfriends = 0 | ||
for k,v in pairs(soundscape[x][y][z]) do | for k,v in pairs(soundscape[x][y][z]) do | ||
if ob["species"] == k["species"] and k ~= ob and (not k["isplayer"]) then --not k isplayer for sanity purposes. | if ob["species"] == k["species"] and k ~= ob and (not k["isplayer"]) then --not k isplayer for sanity purposes. | ||
sound = funcs.totalsoundfromob(k,x,y,z) | sound = funcs.totalsoundfromob(k,x,y,z) | ||
totalstrength = totalstrength + sound | totalstrength = totalstrength + sound | ||
numfriends = numfriends + 1 | numfriends = numfriends + 1 | ||
end | end | ||
end | end | ||
return totalstrength, numfriends | return totalstrength, numfriends | ||
end | end | ||
--ob contains ob.x and ob.y and ob.z, targ contains targ.x,y,z etc. | --ob contains ob.x and ob.y and ob.z, targ contains targ.x,y,z etc. | ||
--if called without "targ" object, it moves to a random square. | |||
function funcs.moveenemytowards(ob,targ) | function funcs.moveenemytowards(ob,targ) | ||
local numfriends | local numfriends | ||
local soundonhomesquare, numfriends = funcs.totalsoundatspot(ob,ob.x,ob.y,ob.z) | local soundonhomesquare, numfriends = funcs.totalsoundatspot(ob,ob.x,ob.y,ob.z) | ||
local distances | local distances | ||
if targ and targ.name then print("Targ name "..targ.name) end | if targ and targ.name then print("Targ name "..targ.name) end | ||
if targ then funcs.pathfindfromto(ob,targ) distances = ob.distances end --gets a path... checks for a new path every so often. | if targ then funcs.pathfindfromto(ob,targ) distances = ob.distances end --gets a path... checks for a new path every so | ||
often. | |||
local soundscape = data.soundscape | local soundscape = data.soundscape | ||
local totalsound | local totalsound | ||
local oldx,oldy,oldz = ob.x,ob.y,ob.z | local oldx,oldy,oldz = ob.x,ob.y,ob.z | ||
local x local y | local x local y | ||
local z | local z | ||
local lowestdistance = {distance = 1000000 ,x=oldx,y=oldy,z=oldz} --HARDCODED LIMIT | local lowestdistance = {distance = 1000000 ,x=oldx,y=oldy,z=oldz} --HARDCODED LIMIT | ||
local iterations = 0 | local iterations = 0 | ||
local movementintelligence | local movementintelligence | ||
if ob.movementintelligence then movementintelligence = ob.movementintelligence | if ob.movementintelligence then movementintelligence = ob.movementintelligence | ||
else movementintelligence = 20 end | else movementintelligence = 20 end | ||
while(iterations < movementintelligence) do | while(iterations < movementintelligence) do | ||
iterations = iterations + 1 | iterations = iterations + 1 | ||
x = math.random(ob["x"]-1,ob["x"]+1) | x = math.random(ob["x"]-1,ob["x"]+1) | ||
y = math.random(ob["y"]-1,ob["y"]+1) | y = math.random(ob["y"]-1,ob["y"]+1) | ||
z = math.random(ob["z"]-1,ob["z"]+1) | z = math.random(ob["z"]-1,ob["z"]+1) | ||
if ((not targ) and (not funcs.checkforblockpassageofpath(x,y,z))) or (targ) then | if ((not targ) and (not funcs.checkforblockpassageofpath(x,y,z))) or (targ) then | ||
local relativevalue = 0 | local relativevalue = 0 | ||
if ob.activateherding then totalsound = funcs.totalsoundatspot(ob,x,y,z) end | if ob.activateherding then totalsound = funcs.totalsoundatspot(ob,x,y,z) end | ||
if targ and not distances then return end --at destination already | if targ and not distances then return end --at destination already | ||
if ((not targ) or (distances and distances[x] and distances[x][y] and distances[x][y][z])) and not funcs.checkforedgeofmap(x,y) then --was and distances[x][y][z] | |||
if ((not targ) or (distances and distances[x] and distances[x][y] and distances[x][y][z])) and not | |||
funcs.checkforedgeofmap(x,y) then --was and distances[x][y][z] | |||
if targ then relativevalue = distances[x][y][z] + relativevalue end | if targ then relativevalue = distances[x][y][z] + relativevalue end | ||
local soundvariable | local soundvariable | ||
local usesoundvariable = true | local usesoundvariable = true | ||
if ob.tightgroups then soundvariable = ob.prefersoundlevel * numfriends end | if ob.tightgroups then soundvariable = ob.prefersoundlevel * numfriends end | ||
if ob.loosegroups then soundvariable = ob.prefersoundlevel end | if ob.loosegroups then soundvariable = ob.prefersoundlevel end | ||
if ob.prefersoundlevel == 0 then usesoundvariable = false end | if ob.prefersoundlevel == 0 then usesoundvariable = false end | ||
if (not targ) and ob.activateherding and soundonhomesquare == 0 then --if i dont hear anything and have no target, choose a random square. | |||
if (not targ) and ob.activateherding and soundonhomesquare == 0 then --if i dont hear anything and | |||
have no target, choose a random square. | |||
relativevalue = relativevalue - (math.random(1,1000) * 10) | relativevalue = relativevalue - (math.random(1,1000) * 10) | ||
elseif ob.activateherding and (usesoundvariable and (soundonhomesquare < (soundvariable)) or not usesoundvariable) and soundonhomesquare > 0 then --if sound is lower than threshhold, herd | |||
if math.random(1,ob.herdingtendency) == 1 then relativevalue = relativevalue - (totalsound * 10) end --herd according to a fraction of times ob.herdingtendency should be 10 or 20 or 5 or whatever. | elseif ob.activateherding and (usesoundvariable and (soundonhomesquare < (soundvariable)) or not | ||
elseif (not targ) then --if | usesoundvariable) and soundonhomesquare > 0 then --if sound is lower than threshhold, herd a FRACTION of the time.. | ||
if math.random(1,ob.herdingtendency) == 1 then relativevalue = relativevalue - (totalsound * 10) end | |||
--herd according to a fraction of times ob.herdingtendency should be 10 or 20 or 5 or whatever. If I'm beyond the sound range | |||
--by not herding, I won't herd from that point until I hear more sound.. | |||
elseif (not targ) then --if I don't care about sound, move randomly. | |||
relativevalue = relativevalue - (math.random(1,1000) * 10) --was + | relativevalue = relativevalue - (math.random(1,1000) * 10) --was + | ||
end | end | ||
if relativevalue < lowestdistance["distance"] then --go towards high strength sound | if relativevalue < lowestdistance["distance"] then --go towards high strength sound | ||
lowestdistance = {["distance"] = relativevalue,["x"]=x,["y"]=y,["z"]=z} | lowestdistance = {["distance"] = relativevalue,["x"]=x,["y"]=y,["z"]=z} | ||
end | end | ||
end | end | ||
end --ends if not targ... | end --ends if not targ... | ||
end --ends iterations over intelligence... | end --ends iterations over intelligence... | ||
funcs.moveobject(ob,lowestdistance["x"],lowestdistance["y"],lowestdistance["z"]) | funcs.moveobject(ob,lowestdistance["x"],lowestdistance["y"],lowestdistance["z"]) | ||
end | end</syntaxhighlight></div> | ||
---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ||
--It uses "soundonhomesquare" to prevent a bug that caused creatures to prefer to stay a certain distance AWAY from each other. LOL. | |||
--so as you can see the "relative value" of the square gets changed depending on how much sound is detected nearby. | --so as you can see the "relative value" of the square gets changed depending on how much sound is detected nearby. | ||
Hope this helps someone! Check out my game Ascii Wilderness, which is open source LUA. | Hope this helps someone! Check out my game Ascii Wilderness, which is open source LUA. | ||
http://asciiwilderness.blogspot.com/p/ascii-wilderness.html | http://asciiwilderness.blogspot.com/p/ascii-wilderness.html | ||
In this game, the deer properly herd together based on their internal variables. This code also depends on some internal herding variables from the participants such as "activateherding = 1" and "tightgroups" or "loosegroups" = 1 and "prefersoundlevel" = amount | |||
In this game, the deer properly herd together based on their internal variables. This code also depends on some internal herding | |||
variables from the participants such as "activateherding = 1" and "tightgroups" or "loosegroups" = 1 and "prefersoundlevel" = amount | |||
and also "herdingtendency" = 3- 10 | and also "herdingtendency" = 3- 10 | ||
Using this code, my ghouls will wait up for other nearby ghouls before closing in for the attack. Sweet! | Using this code, my ghouls will wait up for other nearby ghouls before closing in for the attack. Sweet! | ||
By the way, this is processor hungry, and a good solution would be some kind of "auditory memory" for your denizens, so sounds wouldnt have to be "generated" each turn - which is where your processor gets used up, if you have lots of denizens all generating sounds, thats a lot of squares to check and values to assign. I'm planning some kind of auditory memory now for Ascii Wilderness. | |||
[[Category:Developing]] |
Latest revision as of 01:22, 29 June 2018
This article is about getting your denizens to herd together to a greater or lesser degree and is by Russell Ackerman. You can download the full code by downloading the game Ascii Wilderness: http://asciiwilderness.blogspot.com/p/ascii-wilderness.html check out "ghoul.lua"
I thought I would share my success at getting my deer to herd in my roguelike, I did it by doing this: This article relates to the LUA programming language.
The deer herd together by making a sound each turn, and then when they move to another square, they value those squares according to how much sound there is (but only checking sound for a fraction of the turns that it moves) and move onto the square with the most sound from it's "friends"
"makesound" currently gets called once per turn per creature, soundscape gets nilled out each game turn and re-initailized with [x][y][z] values of the map, prepared to take new entries., and moveenemytowards selects a square based on sound and distance to destination information. the strength of herding is mostly controlled by monster.herdingtendency. Sound propogates out from a square, dropping information about itself. The function is as follows:
Intensity is generally an integer. At intensity 20, the sound will travel outwards 20 squares and use a lot more processor than intensity 5, per se.
function funcs.makesound(intensity,activator,x,y,z)
local objectloc = data.objectloc --contains [x][y][z][object with internal data]
local working, future = {}, { {x,y,z} }
local soundscape = data.soundscape --where your sound data is stored, per tile [x][y][z][the object][thesound - arbitrary key][various keys like strength of the sound]
local finished = {}
local insert = table.insert
local tiles = data.tiles
local checkforedgeofmap = funcs.checkforedgeofmap --so sound doesnt go off the edge of your limited world.
finished[x] = {}
finished[x][y] = {}
finished[x][y][z] = 1
soundscape[x][y][z][activator] = soundscape[x][y][z][activator] or {} --for moveenemytowards
insert(soundscape[x][y][z][activator],{["strength"] = intensity})
local counter = intensity
while(counter <= intensity and counter >= 1) do
counter = counter - 1 --this keeps track of how strong the sound is when it gets to the square.
working,future = future, {}
local x,y,z
for k,v in pairs(working) do
for x = v[1]- 1,v[1] + 1 do
finished[x] = finished[x] or {}
for y = v[2] -1,v[2] + 1 do
finished[x][y] = finished[x][y] or {}
for z = v[3] - 1,v[3] + 1 do --now we're iterating over the nine squares around the center square, on the first iteration.
if z < data.amountofzlevels and z >= 1 and (not checkforedgeofmap(x,y)) and (not finished[x][y][z]) and
(not tiles[x][y][z]["filled"]) and (not tiles[x][y][z]["open"]) then
finished[x][y][z] = 1
insert(future,{x,y,z})
soundscape[x][y][z][activator] = soundscape[x][y][z][activator] or {} --for moveenemytowards
insert(soundscape[x][y][z][activator],{["strength"] = counter})
end
end
end
end
end
end
end
Data about the "soundscape" is stored, for now, just with the data of who made the sound and how intensly it was heard at the square. soundscape[x][y][z][the object that activated it][irreleveant key][strengthofsound] and any other pertinent variables about the noise would also be stored there.
To use this data in pathfinding to get your denizens to herd, simply do the following:
1/x times that the creature activates pathfinding to choose a square with the shortest path, simply weight the RELATIVE VALUE of those squares. Squares with more noise generated by creatures who are the same species as me should be weighted with an amount relative to the total noise on that square from those animals except myself, weighted to be MORE valuable, but only one out of x times - this fractional usage allows the creature to break from the herd and stop herding behavior once it cant hear any sound at all from those of its own species. Its a simple matter of using a function like "total_sound_at_square_except_me(me,x,y,z).
My code for moving the denizens is at follows. the table "Distances" is a table of data in the form distances[x][y][z] = amount, where distances[x][y][z] is the distance of a particular square to the destination square. It's a little hokey but it's pretty simple too:
All I do is subtract sound from distance, and select the square with the "lowest" relative value to move the denizen to.
function funcs.totalsoundfromob(ob,x,y,z)
local soundscape = data.soundscape
local totalstr = 0
for k2,v2 in pairs(soundscape[x][y][z][ob]) do
totalstr = totalstr + v2["strength"]
end
return totalstr
end
function funcs.totalsoundatspot(ob,x,y,z) --totals sounds at spot from "my species"
local soundscape = data.soundscape
local sound
local totalstrength = 0
local numfriends = 0
for k,v in pairs(soundscape[x][y][z]) do
if ob["species"] == k["species"] and k ~= ob and (not k["isplayer"]) then --not k isplayer for sanity purposes.
sound = funcs.totalsoundfromob(k,x,y,z)
totalstrength = totalstrength + sound
numfriends = numfriends + 1
end
end
return totalstrength, numfriends
end
--ob contains ob.x and ob.y and ob.z, targ contains targ.x,y,z etc.
--if called without "targ" object, it moves to a random square.
function funcs.moveenemytowards(ob,targ)
local numfriends
local soundonhomesquare, numfriends = funcs.totalsoundatspot(ob,ob.x,ob.y,ob.z)
local distances
if targ and targ.name then print("Targ name "..targ.name) end
if targ then funcs.pathfindfromto(ob,targ) distances = ob.distances end --gets a path... checks for a new path every so
often.
local soundscape = data.soundscape
local totalsound
local oldx,oldy,oldz = ob.x,ob.y,ob.z
local x local y
local z
local lowestdistance = {distance = 1000000 ,x=oldx,y=oldy,z=oldz} --HARDCODED LIMIT
local iterations = 0
local movementintelligence
if ob.movementintelligence then movementintelligence = ob.movementintelligence
else movementintelligence = 20 end
while(iterations < movementintelligence) do
iterations = iterations + 1
x = math.random(ob["x"]-1,ob["x"]+1)
y = math.random(ob["y"]-1,ob["y"]+1)
z = math.random(ob["z"]-1,ob["z"]+1)
if ((not targ) and (not funcs.checkforblockpassageofpath(x,y,z))) or (targ) then
local relativevalue = 0
if ob.activateherding then totalsound = funcs.totalsoundatspot(ob,x,y,z) end
if targ and not distances then return end --at destination already
if ((not targ) or (distances and distances[x] and distances[x][y] and distances[x][y][z])) and not
funcs.checkforedgeofmap(x,y) then --was and distances[x][y][z]
if targ then relativevalue = distances[x][y][z] + relativevalue end
local soundvariable
local usesoundvariable = true
if ob.tightgroups then soundvariable = ob.prefersoundlevel * numfriends end
if ob.loosegroups then soundvariable = ob.prefersoundlevel end
if ob.prefersoundlevel == 0 then usesoundvariable = false end
if (not targ) and ob.activateherding and soundonhomesquare == 0 then --if i dont hear anything and
have no target, choose a random square.
relativevalue = relativevalue - (math.random(1,1000) * 10)
elseif ob.activateherding and (usesoundvariable and (soundonhomesquare < (soundvariable)) or not
usesoundvariable) and soundonhomesquare > 0 then --if sound is lower than threshhold, herd a FRACTION of the time..
if math.random(1,ob.herdingtendency) == 1 then relativevalue = relativevalue - (totalsound * 10) end
--herd according to a fraction of times ob.herdingtendency should be 10 or 20 or 5 or whatever. If I'm beyond the sound range
--by not herding, I won't herd from that point until I hear more sound..
elseif (not targ) then --if I don't care about sound, move randomly.
relativevalue = relativevalue - (math.random(1,1000) * 10) --was +
end
if relativevalue < lowestdistance["distance"] then --go towards high strength sound
lowestdistance = {["distance"] = relativevalue,["x"]=x,["y"]=y,["z"]=z}
end
end
end --ends if not targ...
end --ends iterations over intelligence...
funcs.moveobject(ob,lowestdistance["x"],lowestdistance["y"],lowestdistance["z"])
end
--It uses "soundonhomesquare" to prevent a bug that caused creatures to prefer to stay a certain distance AWAY from each other. LOL. --so as you can see the "relative value" of the square gets changed depending on how much sound is detected nearby.
Hope this helps someone! Check out my game Ascii Wilderness, which is open source LUA.
http://asciiwilderness.blogspot.com/p/ascii-wilderness.html
In this game, the deer properly herd together based on their internal variables. This code also depends on some internal herding variables from the participants such as "activateherding = 1" and "tightgroups" or "loosegroups" = 1 and "prefersoundlevel" = amount and also "herdingtendency" = 3- 10
Using this code, my ghouls will wait up for other nearby ghouls before closing in for the attack. Sweet!
By the way, this is processor hungry, and a good solution would be some kind of "auditory memory" for your denizens, so sounds wouldnt have to be "generated" each turn - which is where your processor gets used up, if you have lots of denizens all generating sounds, thats a lot of squares to check and values to assign. I'm planning some kind of auditory memory now for Ascii Wilderness.