Bot Of The Month Club
As is traditional each year, this month we talk with Andras again about the
Easter Egg "Hunt Bot." The Easter Egg hunts in Storage (and other worlds)
are legendary in ActiveWorlds. Andras (one of the originators of this
tradition) tells us all about the bot, what it does, and even how it works!
Thanks, Andras, for such a great yearly tradition :-)
|
Hamfon & The Hambots
|
Know of a bot that you'd like to see featured as the Bot of the Month? If so, contact us at newsletter@activeworlds.com.
HuntBot - the answer to how to make an object hunt easy for the hunters
AND to the host.
History
What does the HuntBot do
Design Phase
Implementation Phase
The GUI
Tricks
Improvements
Statistics
How to organize a Hunt?
History:
We started to make egg hunts in Storage world 5 years ago after the very
first Egg Hunt in Winter then added Christmas Present hunts as a Storage
world event the same year. The first few hunts were based on manual registration
and tedious recording of who found how many objects. Even the count of
the findings were handled manually. Then I decided a bot can do this task
much quicker than a human being, so I started to create the HuntBot. The
bot is written in C++ under Borland's CBuilder 5.
What does the HuntBot do:
The basic role of the bot is to query the world for the objects which are
the target of the hunt (eggs, gifts, etc.). It keeps track of all visitors
entering and leaving. When a hunter clicks on an object, the bot checks
if this is a hunt target and if the hunter is clicking on it the first
time. It announces to the hunter that a new object found and tallies it's
findings for him.
Design phase:
As some probably know, I tend to design all of my programs before the actual
coding. It gives me the advantage of having a full view of the idea and
I lay down the specification based on all the requirements. Every improvement
or addition that comes later can nicely fit into the design when I've done
a careful specification at the beginning.
Ok, what are the goals we want to accomplish?
-
Lets make the bot so the user can use the mouse instead of typing, freeing
up his keyboard for chatting. This is extremely important because the hunt
is a social gathering and people like to talk to each other. Why force
them to change the keyboard focus causing strange messages to everyone?
-
Let the bot keep track of each individual's tally. Hunters like to see
where are they in the hunt!
-
Let the bot allow the users to log out of Active Worlds (some of you still
need some sleep, do you?) and continue the hunt where they left off.
-
Make the bot "Error proof"! The hunters will probably kill you if the bot
crashes and forgets all the eggs they found!!
-
Try to create a bot which is hard to cheat. Some people still like to get
"free dinner". Don't let them!
-
Keep logs! More logs! Even more!!!! The only way to find cheaters is if
you can analyze the logs. The more data you have the more info you can
gather about the hunt.
-
What if the bot crashes? Do the hunters have enough patience to wait until
you reload the bot (if you are not asleep at the keyboard).
Will they wait for the bot to do the world query again to get all the object's
locations? Unlikely, so save all info immediately after it
is collected and reload it when the bot restarts. (A reload from your HD
is much faster than from the net.) Have the bot check itself to see
if it is still running/connected to the world. If not, force the restart
programmatically so no human intervention needed.
-
Try to minimize the network traffic. Only the bare minimum of signals should
be collected from the server to avoid the server's (and the bot's) computer
overloading.
-
Optimize the bot's action to make it more responsive to the users. This
phase can be done later, not during the design phase, because you don't
know where the bottleneck will be up front.
-
Finally, have a tool built-in to provide immediate scoring for announcing
the winners. The faster you satisfy their curiosity, the happier the hunters
are. What we learned during the hunts is that it is better to keep the
top scores secret until the end of the game. It keeps lower scoring hunters
staying in the game longer (and at least enjoying the chit-chat).
Implementation phase: (for those who are
not interested in technical details can skip this section)
The first thing is always the data structure! If you screw that
up you'll have a hard time accomplishing your goals. When you design
them, it's better to add more information to them than less. It will pay
later and the computer memory is free nowadays <oopsie> (well, pretty
cheap anyway!).
Our environment calls for dynamic data structure. You can't decide up
front how many visitors/players will be there or the maximum amount of
clicks/players.
I started with a basic bi-directional link class which is the
parent class to all data structures:
class TMyQueue
{
private:
TMyQueue *predecessor,*successor;
public:
__property TMyQueue *succ = {read=successor, write=successor};
__property TMyQueue *pred = {read=predecessor, write=predecessor};
__fastcall TMyQueue () { predecessor=successor=this;
};
void __fastcall TMyQueue::Insert(TMyQueue *after);
TMyQueue * __fastcall TMyQueue::Remove();
TMyQueue * __fastcall TMyQueue::First();
TMyQueue * __fastcall TMyQueue::Next();
bool __fastcall TMyQueue::Finished(TMyQueue *q);
};
There are 4 descendant of this class:
1, Name list:
class TNameQueue : public TMyQueue
{
public:
char *pszName;
int iType;
}
This class is used to keep the generic names of the hunt object
(like egg1.rwx, rjegg27.rwx, etc.). The program uses this queue when it
does the query of the world to select the target objects from the world
database. Don't forget to delete the char string in the destructor! This
frequently forgotten action results memory leaks.
2, Object descriptor:
class TObjQueue : public TMyQueue
{
public:
char *pszModel,*pszDescription,*pszAction;
int iX,iY,iZ,iYaw,iObjectNumber,iOwner, iTimestamp,iCellx,iCellz;
}
TObjQueue is the exact description of the target objects scattered
around the bot's visibility range..
3, Avatar descriptor:
class TAvatar : public TMyQueue
{
public:
int iSession,iOldSession,iCitNumber;
int iClickCount,iTotalTime;
char *pszName;
int iIP;
int iNumobj;
bool bVerbose,bCrackedBrowser;
TObjQueue *ObjHash[HASH_SIZE];
}
All visitors get a TAvatar object assigned from the beginning of
the hunt. The avatar has an object queue to hold those targets which the
hunter already clicked. That object queue is carved up into a hash table
which I'll describe in the Tricks section.
4, Bot descriptor:
class TBot : public TAvatar
{
private:
void *instance;
int iSequence[3][3]; // total zone sequence
value
bool bUpToDate;
public:
int iSector_x, iSector_z;
int iCell_x, iCell_z;
}
Please note that the TBot is a descendant of the TAvatar class. All properties
of the avatar are inherited by the bot but used slightly differently. There
is no use for a few variables (like bVerbose or iNumobj) but I was too
lazy to create two independent classes. The object hash table is holding
all the target objects which the bot covers. The bot's sector and cell
address is pre calculated to avoid lengthy mathematical operations.
Based on the user's selection, there can be more than one bot instance
to cover the whole hunting territory. With a single bot one can conduct
a hunt within a 400x400m area which is a single bot's maximum visibility
range. We had 2000x2000m to cover, so in our hunt we used 25 bot instances!
Now a few world about the program's structure:
The program has all the necessary callbacks installed for the job and
it had a 100 ms timer to call aw_wait(0) regularly.
aw_event_set (AW_EVENT_AVATAR_ADD, handle_avatar_add);
aw_event_set (AW_EVENT_AVATAR_DELETE, handle_avatar_delete);
aw_event_set (AW_EVENT_AVATAR_CHANGE, handle_avatar_change);
aw_callback_set (AW_CALLBACK_ADDRESS, handle_IP_address);
aw_event_set (AW_EVENT_CHAT, handle_chat);
aw_event_set (AW_EVENT_OBJECT_CLICK, handle_object_click);
aw_event_set (AW_EVENT_WORLD_DISCONNECT,handle_world_disconnect);
aw_callback_set (AW_CALLBACK_QUERY, handle_query);
aw_event_set (AW_EVENT_CELL_BEGIN, handle_cell_begin);
aw_event_set (AW_EVENT_CELL_OBJECT, handle_cell_object);
aw_callback_set (AW_CALLBACK_CELL_RESULT, handle_cell_result
);
I started the coding itself with a "Top down" coding method so I could
debug while creating it. First I made the world query part work. When the
query was operating as I wanted, I created a database from the query, so
later I don't have to query the world again. That saved a lot of time for
me! Each bot simply dumps its object queue to a separate file after the
query finished (huntobj000.txt, huntobj001.txt ...). At the restart, the
user can select a check box to "AutoLoad" those queues from files instead
of doing the query again. The object file format is similar to the standard
propfile format, except it has the object number embedded too:
-392249089 0 976710621 -60475 20 -58900 900 11 0 0 prpeg44.rwx
The next obvious step was to teach the bot how to handle the clicking.
I did some cheating so the bot can receive the AW_EVENT_OBJECT_CLICK
message without any queries. When the hunter clicks on an object the callback
method gives us the avatar session number, the object number and the cell's
x and z coordinates the object is residing in. The bot then checks if this
object number is in it's object queue. If it is not found there (was not
a target object!) it simply discards the message but increments the corresponding
TAvatar's click counter. When the object is found in it's queue, the bot
searches the corresponding avatar's object queue to see if the object is
residing there. If the object is not found, it is then placed in the avatar's
object queue, the object counter is incremented and a message sent to the
hunter if he did not disable the bot whispers. (There is a special case
when the object is not counted as a new one but that is described in detail
in the
Improvement section.) Each new
target click is recorded in the click.log file for archiving and retrieval
purpose later. When the bot is restarted, it reads this click.log file
and fills up the corresponding queues with the information. The hunter's
data is preserved!!
Bots should have names but I was too lazy to implement a feature to
assign individual names for all bot instances. I used one name for the
center bot and another name for all the other instances which I concatenated
a simple sequence number to. They are the "helper" bots for the center
bot. e.g.: [EasterBunny], [LittleBunny1], [LittleBunny2], etc.
I implemented a chat log to help with debugging the program so each
bot instance has it's own log file and the program has it's chatlog file
under the name of the bot instance.
While I was at the chat management, I decided it would be good to control
the bot with whispers. I created "Authorized PS" accounts within the bot:
a name list which holds the citizen name. Each citizen on this list can
control the bot with whisper if he/she is a PS in the world and is wearing
one of the "Special" avatars. The bot INI file should hold at least the
owner's citizen name, so he/she can add another user to this list with
whispers. There is a detailed help message sent to the AuthorizedPS if
he/she whispers help to the bot.
Things you can control from whisper contains:
"TOURIST:ENABLE/DISABLE"
"AUTOSTART:ENABLE/DISABLE"
"STARTHUNT: - enables click logging"
"STOPHUNT: - disables click logging"
"CHATLOG:ENABLE/DISABLE"
"NOCHEATERS:ENABLE/DISABLE - disable them from the score
list"
"CHEATWARNING:ENABLE/DISABLE"
"ADDAUTHORIZEDPS:citizenname - to add another citizen to
the bot control list."
"DELETEAUTHORIZEDPS:citizenname - to remove a citizen from
the bot control list."
"LISTAUTHORIZEDPS: - to list authorized citizens"
"TOP: whispers the top 10 hunters"
"ANNOUNCERESULT:# broadcasts the #-th hunter's results"
The next step was to create an INI file to hold all the controls for the
bot. Sometimes it's faster to fill the INI file with data than do the one
by one fill on the GUI. There are two hidden fields from the GUI and they
are accessible only within the INI file. The 2 fields are the Universe
URL and Universe Port. The program defaults to the Active Worlds parameters.
The last step was the error recovery process. The bot detects if it
loses connection to the server and it tries to restart itself after 60
seconds. A successful restart reloads all the saved data (like the clicks
for the avatars and the objects for the bots) and the hunt continues.
With all of those implemented features I had a fairly good working program
which was used for the next egg hunt in Storage world. Running for 24 hours
and surviving several crashes, I collected enough real data to improve
the program. Even with the occasional crashes the hunt was a success.
The GUI
A few pictures are worth a thousand words. I don't consider myself a good
GUI designer, so this program's look and feel leaves something to be desired.
:) Fortunately the interface is only visible by the bot's user, not to
the hunters.
This is how the program looks before the hunt:
When adding new objects to the objet name list you'll get the following
popup window:
While in the initial phase - you want to modify the default greeting
messages:
After you start the bot, each bot will have it's own Tab field containing
it's log, position and communication fields:
During the hunt you can check the current scores either inworld by whispering
to the bot or in the GUI:
When the hunt is finished, you can announce the winners by either whispering
or with double clicking on the score line (Don't forget to check the "DoubleClick
Announce" check box!)
Then it comes to generating the appropriate http document(s) for your
web page:
Tricks
In this section I would like to tell a few tricks which made the
bot even better:
-
I found that if you send a lot of messages from the bot to the hunter
(like the help messages), it can stop responding to the clicks for a while
so I used the 100 ms timer to send the long messages in pieces - line by
line.
-
Another problem I found was that while flushing the RichEdit content to
the disk (logging!) the bot could lose the connection due to some slow
disk I/O operation. The solution was to flush it every time the length
reached 2K bytes so the disk I/O could be fast enough. The only synchronous
disk write was the click logging because I did not want to lose any clicks
if the bot crashed.
-
How to cheat the SDK so that the bot has an up to date query table, so
it starts to receive click events? Remember - I have all the object data
in memory, loaded from a previous query, so I did not really want to requery
the world. The bot saves all the sequences to the huntseq.txt file too,
so I reload those from disk and when I invoke the aw_query() the bot is
already up to date! It turned out later that if you fill the bot's sequence
with -1 - no query will happen but the bot is up to date with the world
(it's sequence number exceeds the current sequence number).
-
One more thing I found out in a hard way: When you flush the RichEdit object
(the logs space holder) to the disk, you have to be careful with the amount
you write at once. The lengthy file write operation can cause messages
lost from the world server. After some experiments I decided to flush the
log files when their buffer reaches 4K Bytes.
Improvements:
We had a few hunts behind us already and I learned that the program needs
improvements. As the number of eggs we hid inworld increased, the search
for the object in the bot and the avatar's queue started to grow too long.
I had to do something to make the search faster. Imagine, in our last hunt
we had in excess of 19,000 eggs! That means searching 19,000 objects to
find the one they clicked! Well - it just won't work :)
The solution was to introduce hash tables for the avatars and for the
bots. Each hash table has 256 queues. I used the random number AW assigns
to the object to select within the queues with simply masking down the
lower 4 bits forming an integer index to the hash table. I hoped the random
numbers have even distribution and it wasn't too bad. At last year's hunt
I used only a 16 element hash table and the most dense area had the following
queue lengths:
Total objects found :2270
0. Hash queue length= 146
1. Hash queue length= 151
2. Hash queue length= 151
3. Hash queue length= 142
4. Hash queue length= 155
5. Hash queue length= 142
6. Hash queue length= 129
7. Hash queue length= 146
8. Hash queue length= 145
9. Hash queue length= 147
10. Hash queue length= 159
11. Hash queue length= 128
12. Hash queue length= 124
13. Hash queue length= 132
14. Hash queue length= 125
15. Hash queue length= 148
As you can see the distribution was pretty even. The queue length is still
too large this is why I finally made it to 8 bits (256 queues) which reduced
the length to an average 9 objects. With this trick I was able to reduce
the CPU load by %90!!!! Tremendous savings, isn't it?
During the hunt we found the need to communicate to all of our hunters
but they were spread around the P100 world. I did not want to use the world's
Welcome Message because not every hunt will be hold in a world where you
can change it so I implemented a broadcast feature. You can whisper the
message to the bot and it will relay it through all the bots in the world
effectively reaching all hunters.
We had to find ways to avoid cheating. In the past several people were
using "cracked" browser, so they can override the world's features. There
are two features built into the bot:
-
If someone uses a "cracked" browser so it can select an object event the
"Object Selection" is off - triggers a flag that that hunter is cheating.
-
We hid some eggs inside other objects (like a small black box just barely
larger than the egg itself) which has a "cheat" word in the action field.
This method helped us to discover those who replaced the OP with an OP
on their disk so only the egg objects are visible. If someone clicked on
those objects it triggered the cheating flag too. One warning though: It
can happen that a person on a slower machine will get the surrounding box
rendered much later than the egg itself, so try to place those objects
to the area where the visitors first land in the world - effectively caching
them. When such cheats found do some checking in the logs to see if it
happened too frequently or not. The bot's owner can turn the cheat warnings
on or off of course.
Hunters wanted results IMMEDIATELY! To do a manual recount or sorting would
take too long (remember the 2000 election?). I added a new feature to the
bot that it can announce the winners from the sorted Score list by whispering
the proper command to it.
Another tedious work was to create a web page which holds the list of
the winners and (possibly) all other participants data. In the past Daphne
manually created/filled out those tables! I added another feature now that
the bot can generate the html tables for a web page. All you have to do
is cut and paste it to your web page.
Statistics from the latest hunt (3/30/2002) during
the 36 hours hunt period:
Number of eggs hidden |
19598 |
Total visitor number |
634 |
Max. number of egg found |
9378 |
Total number of clicks during the hunt |
711477 |
Maximum number of clicks by one hunter |
38992 |
Total number of eggs found by all hunters |
224211 |
Total "User time" spent in Storage |
1178h |
Average time a hunter spent in Storage |
1h 51' |
Average number of eggs found per hunters |
353 |
Average number of clicks per hunter |
1122 |
Average CPU Usage during the hunt (together with the world server) |
%14 |
Total CPU time during the 36 hours |
1h 28' |
Total world server network traffic |
2350 MBytes |
How to organize a Hunt?
FIRST THINGS FIRST:
-
Decide where to hold it and make sure your world's user limit is high enough
that potential hunters won't be "locked out".
-
Decide when to hold it and how long it will last. (Don't forget that you
have to sleep sometime too!)
-
Decide what to hunt for. (Having a theme helps create more interest.)
-
If you are going to use any bots, make sure you know how to operate and
control them!
-
Decide whether or not to offer prizes and if you are offering prizes, what
will they be and in what order they will be awarded; 1st, 2nd, 3rd, etc..?
SETTING UP THE HUNT:
How are you going to hide objects without potential hunters watching
you hide them? Are you going to close your world while you hide the
objects or do you have another world with the same build in it that you
can set the hunt objects up in then move to the actual hunt world just
before the hunt?
Will you need some trusted people to help you hide the objects or will
you set up a BuildBot to disperse objects?
What are the "rules" for hiding objects? (inside other objects; below
ground level; so high they can't be seen while standing on the ground)
Your hunters will want to have a general idea of where to look and where
not to look.
Will you or your helpers need a map of hunt object distribution to
be sure there is fairly even coverage? (If the hiding of objects
is going to happen over several days and be done by several people, a map
of object distribution can be a big help.)
A web page supporting the event is very helpful. You can point
people to the URL to get complete information about the hunt.
They will know what they need to know in order to make the hunt and it's
rules clear to them, plus it makes a host's job easier if they're able
to point to a URL..
ADVERTISE, ADVERTISE, ADVERTISE!!!
-
Signs/graphics in your world and your friend's worlds.
-
AW Newsletter
-
AW Community Calendar
-
Newsgroups: AW's and
Andras'
-
Supporting web page
-
Word of mouth/telegrams
-
and any other source you can think of!
AFTER THE HUNT:
Will you be able to log the world or obtain the log so you can check
for possible cheating? (cheaters should never win!)
If you are not using a bot to keep track of found objects you will
need a method by which your hunters can prove what objects they've
found. Code words or phrases on each hidden object that the
hunter has to list and send to you is one way. Different filenames
for each object in the hunt is another way but again, the hunter has to
make a list and send it to you. You, of course get the job of checking
the accuracy of the lists and counting the number of accurate items on
them before any winners can be announced. :o)
Do let your hunters have an idea about how soon after the end of the
hunt the winners will be announced. Hunters are a rather impatient
bunch and want to know as soon as possible. :o)
Signs at GZ of who the winners are is always welcome by people that
participated. A web page listing the tallies of all that hunted (or
tallies over a certain amount) is also welcome to so ppl can compare how
they did against other hunters.
Don't forget to thank your helpers!
The hunt is not fully complete till the winners have their prizes.
REMEMBER:
Holding a hunt is mostly a terrific social event; the hunting is actually
pretty secondary for many. Having a host at Ground Zero to answer
questions and say hello helps the sociability a lot. Even hunters
that don't win a prize usually come away from the event feeling very good
about the time spent. :o) (Don't YOU forget to enjoy
the hunt and your visitors too!)
Courtesy of Andras and Daphne :)
If you're interested in finding out more about how to use the
SDK, visit the SDK site for information!
|